var cobrandingUrl = "null";
var ticketServerLocation = "https://userservices.vip.symantec.com";
var moTicketServerLocation = "https://userservices.vip.symantec.com";
var chromeExtensionDownloadUrl = "null";
var messagingServerLocation = "https://sedvip-prod-messaging-fe-nginx-b-w";
var entSspUrl = "null";
var idpUrl = "null";
var parentDomainToBeTagged = "null";
var isTrustedDeviceEnabled = "null";
var entVipsUrl = "null";
var isLogEnabled = (typeof console !== "undefined" && typeof console.log !== "undefined" && typeof console.error !== "undefined");
var safariExtensionUrl = "null";
var firefoxExtensionBaseUrl = "null";
var isMOFallbackEnabled = false;
var isNumbersChallengeEnabled = false;
var signInFlowType = "null";
var catalogFromUserServices = catalogFromUserServices || {"message.error.send.securitycode":"Unable to send security code to ${decodedOption}. Try again later.","message.success.install.plugin":"The Symantec Authentication Client Plugin was successfully installed.","dlview.text.download.instruction.content2":"
Run the installer and follow the prompts to install the plugin.
","chrome.add.extension.btn.get.extension":"Get Extension","message.success.send.securitycode":"A security code has been sent to ${decodedOption}. ","oobview.text.title":"Confirm Your Identity","chrom.addextension.instruction.header":"
Install the Symantec Authentication Client Extension
","dlview.text.ie.install.instruction.content":"
Install the Symantec Authentication Client Plugin:
Select Run, or Save to download the Symantec VIP executable file.
Run the software when prompted.
","auview.text.upgrade.complete":"The Symantec Authentication Client Plugin was upgraded successfully.","fnview.text.subtitle":"Name Your Device:","moview.link.no.mobile.device.no.fallback":"Send it again","installpanel.text.extension":"Install Extension","scview.text.title":"Confirm Your Identity","edview.text.chrome.install.instruction.content":"
To install the extension:
Click Get Extension in the Install Extension tab.
Click Add in the resulting Add to Chrome pop-up.
","edview.text.download.instruction.content":"
To remember this device, you must first install the Symantec Authentication Client Extension, and then install the plugin.
","moview.text.title":"Confirm Your Identity","moview.text.no.fallback.error.message":"Authentication failed. Please contact your administrator for assistance.","dialog.button.send":"Send","message.error.invalid.securitycode":"Security code must contain at least 6 numbers with no letters or special characters. Try again.","dialog.button.confirm":"Confirm","scview.checkbox.rememberdevice":"Remember this private device","dialog.button.skip2":"Skip","edview.text.common.install.instruction.content":"
Install the Symantec Authentication Client Plugin:
Select Save File and then OK.
From the Safari Downloads window, open the Symantec VIP executable file.
Double-click the checkmark to install.
","scview.placeholder.securitycode":"Security Code","dialog.button.skip":"Skip for Now","edview.text.download.instruction.content2":"
Continue to visit the Chrome Web Store and click Add to Chrome
","chrome.add.extension.instruction":"
Your browser requires an extension to continue.
To install the extension:
Click Get Extension.
Click Add to Chrome in the resulting Chrome Web Store tab.
Click and Add extension in the installation pop-up.
Once complete, you can close the Chrome Web Store tab.
","chrome.add.extension.error.message":"Unexpected error. Contact your administrator for assistance.","message.error.invalid.credentialname":"Credential name is empty or contains invalid characters. Try again.","dialog.button.remember":"Remember","auview.text.title":"Upgrade Plug-in","scview.checkbox.rememberdevice.note":"You will not need to provide a security code in the future.","oobview.text.subtitle":"Select a way to receive a security code:","mdview.text.instruction.content1":"Your authentication request is approved.","auview.text.upgrading":"Your Symantec Authentication Client Plugin is out of date. For your security we are automatically upgrading. Please wait...","dlview.text.chrome.install.instruction.content2":"
Run the installer and follow the prompts to install the plugin.
","oobview.radio.voice":"Voice Call:","moview.text.numbers.challenge.note":"Approve the request by entering the number on the device to finish signing in.","edview.text.safari.install.instruction.content":"
To install the Symantec Authentication Client extension:
Install the downloaded package.
Enable the extension in Safari (Safari > Preferences > Extensions > Check Symantec Authentication Client extension
Install the Symantec Authentication Client Plugin:
Select Save File.
From the Firefox Downloads window, open the Symantec VIP executable file.
If prompted, confirm you want to open the file.
Run the file when prompted.
","dlview.text.download.instruction.content":"
Download the Symantec Authentication Client Plugin installer.
","mdview.text.title":"Remember Device","dialog.button.download":"Download","scview.link.no.securitycode":"Don't have a security code?","edview.text.firefox.install.instruction.content":"
To install the Symantec Authentication Client extension, click Allow on the Software Installation pop-up in Firefox. Then, click Install.
","mdview.text.instruction.content2":"Do you want to remember this device? You will not receive a push request for authentication in the future.","message.error.install.plugin":"The Symantec Authentication Client Plugin could not be installed. Try again later.","oobview.radio.sms":"SMS Text:","moview.text.subtitle":"A sign in request was sent to the following mobile devices.","scview.text.subtitle":"We Don't Recognize Your Device","scview.label.securitycode":"Security Code:","dialog.button.continue":"Continue","scview.checkbox.rememberdevice.reached.max":"You can only have ${policy.RD_MAX_ALLOWED} remembered device. Contact your administrator to remove an existing remembered device.","cwview.text.chrome.warning":"You must upgrade your Symantec Authentication Client.
This is required due to changes in Google Chrome.
Click Continue, then enter your security code on the next screen to upgrade.
Keep the Remember this private device check box selected to upgrade the client plugin and extension.
This message will appear until you complete the upgrade.
","moview.link.no.mobile.device":"Send it again or Use a security code to sign in","muview.text.upgrade.complete":"The Symantec Authentication Client Plugin was upgraded successfully.","fnview.text.devicename.note":"Enter a simple name that is easy to remember","moview.rememberdevice.reached.max":"You have reached your maximum allowed remembered devices. Contact your administrator to remove an existing remembered device.","muview.text.instruction":"Your Symantec Authentication Client Plugin is out of date. For your security, download and install the latest version.","dialog.button.cancel":"Cancel","fnview.text.devicename.placeholder":"My Device","fnview.text.devicename.confirmation.note":"This device will now be remembered as a trusted device for future sign-in access.","chrome.add.extension.title":"Install Extension","message.error.session.expired":"Session has expired. Enter security code to Sign In.","dlview.text.chrome.install.instruction.content":"
The Symantec Authentication Client Plugin installer has been downloaded.
").append(n.parseHTML(a)).find(d):a)}).complete(c&&function(a,b){g.each(c,e||[a.responseText,b,a])}),this},n.expr.filters.animated=function(a){return n.grep(n.timers,function(b){return a===b.elem}).length};var dd=a.document.documentElement;function ed(a){return n.isWindow(a)?a:9===a.nodeType?a.defaultView||a.parentWindow:!1}n.offset={setOffset:function(a,b,c){var d,e,f,g,h,i,j,k=n.css(a,"position"),l=n(a),m={};"static"===k&&(a.style.position="relative"),h=l.offset(),f=n.css(a,"top"),i=n.css(a,"left"),j=("absolute"===k||"fixed"===k)&&n.inArray("auto",[f,i])>-1,j?(d=l.position(),g=d.top,e=d.left):(g=parseFloat(f)||0,e=parseFloat(i)||0),n.isFunction(b)&&(b=b.call(a,c,h)),null!=b.top&&(m.top=b.top-h.top+g),null!=b.left&&(m.left=b.left-h.left+e),"using"in b?b.using.call(a,m):l.css(m)}},n.fn.extend({offset:function(a){if(arguments.length)return void 0===a?this:this.each(function(b){n.offset.setOffset(this,a,b)});var b,c,d={top:0,left:0},e=this[0],f=e&&e.ownerDocument;if(f)return b=f.documentElement,n.contains(b,e)?(typeof e.getBoundingClientRect!==L&&(d=e.getBoundingClientRect()),c=ed(f),{top:d.top+(c.pageYOffset||b.scrollTop)-(b.clientTop||0),left:d.left+(c.pageXOffset||b.scrollLeft)-(b.clientLeft||0)}):d},position:function(){if(this[0]){var a,b,c={top:0,left:0},d=this[0];return"fixed"===n.css(d,"position")?b=d.getBoundingClientRect():(a=this.offsetParent(),b=this.offset(),n.nodeName(a[0],"html")||(c=a.offset()),c.top+=n.css(a[0],"borderTopWidth",!0),c.left+=n.css(a[0],"borderLeftWidth",!0)),{top:b.top-c.top-n.css(d,"marginTop",!0),left:b.left-c.left-n.css(d,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||dd;while(a&&!n.nodeName(a,"html")&&"static"===n.css(a,"position"))a=a.offsetParent;return a||dd})}}),n.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,b){var c=/Y/.test(b);n.fn[a]=function(d){return W(this,function(a,d,e){var f=ed(a);return void 0===e?f?b in f?f[b]:f.document.documentElement[d]:a[d]:void(f?f.scrollTo(c?n(f).scrollLeft():e,c?e:n(f).scrollTop()):a[d]=e)},a,d,arguments.length,null)}}),n.each(["top","left"],function(a,b){n.cssHooks[b]=Mb(l.pixelPosition,function(a,c){return c?(c=Kb(a,b),Ib.test(c)?n(a).position()[b]+"px":c):void 0})}),n.each({Height:"height",Width:"width"},function(a,b){n.each({padding:"inner"+a,content:b,"":"outer"+a},function(c,d){n.fn[d]=function(d,e){var f=arguments.length&&(c||"boolean"!=typeof d),g=c||(d===!0||e===!0?"margin":"border");return W(this,function(b,c,d){var e;return n.isWindow(b)?b.document.documentElement["client"+a]:9===b.nodeType?(e=b.documentElement,Math.max(b.body["scroll"+a],e["scroll"+a],b.body["offset"+a],e["offset"+a],e["client"+a])):void 0===d?n.css(b,c,g):n.style(b,c,d,g)},b,f?d:void 0,f,null)}})}),n.fn.size=function(){return this.length},n.fn.andSelf=n.fn.addBack,"function"==typeof define&&define.amd&&define("jquery",[],function(){return n});var fd=a.jQuery,gd=a.$;return n.noConflict=function(b){return a.$===n&&(a.$=gd),b&&a.jQuery===n&&(a.jQuery=fd),n},typeof b===L&&(a.jQuery=a.$=n),n});
jQuery.ajaxPrefilter( function( s ) {
if ( s.crossDomain ) {
s.contents.script = false;
}
});
/*!
* polyfill/console.js
*
* Copyright 2019 Symantec Corporation All Rights Reserved.
*
*/
(function() {
var method, fn;
var noop = function () {};
var methods = [
'assert',
'clear',
'count',
'debug',
'dir',
'dirxml',
'error',
'exception',
'group', 'groupCollapsed', 'groupEnd',
'info',
'log',
'markTimeline',
'profile', 'profileEnd',
'table',
'time', 'timeEnd',
'timeStamp',
'trace',
'warn'
];
var length = methods.length;
/* Create a global window.console is not yet existing and assign to local variable */
var console = (window.console = window.console || {});
/* Loop through list of methods backwards (JS optimization) */
while (length--) {
method = methods[length];
fn = console[method];
if (!fn) {
/* If no method exists, set it to a no-op */
console[method] = noop;
} else if (!fn.apply) {
/* If no '.apply' method, then the function is native and must be wrapped (ex: IE9-IE11) */
console[method] = Function.prototype.bind(fn, console);
}
}
}());
/**
* SHA256 Hash Algorithm Plugin
*
* @version 1.0 (06/09/2009)
* @requires jQuery v1.2.6+
* @author Alex Weber
* @copyright Copyright (c) 2008-2009, Alex Weber
* @see http://anmar.eu.org/projects/jssha2/
* @see http://pajhome.org.uk/crypt/md5
*
* Distributed under the BSD License
*
*/
/**
* This plugin is based on the following work:
*
* A JavaScript implementation of the Secure Hash Algorithm, SHA-256, as defined
* in FIPS 180-2
* Version 2.2-beta Copyright Angel Marin, Paul Johnston 2000 - 2009.
* Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
*
*/
(function($) {
var chrsz = 8; // bits per input character. 8 - ASCII; 16 - Unicode
var safe_add = function(x, y) {
var lsw = (x & 0xFFFF) + (y & 0xFFFF);
var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
return (msw << 16) | (lsw & 0xFFFF);
}
var S = function(X, n) {
return ( X >>> n ) | (X << (32 - n));
}
var R = function(X, n) {
return ( X >>> n );
}
var Ch = function(x, y, z) {
return ((x & y) ^ ((~x) & z));
}
var Maj = function(x, y, z) {
return ((x & y) ^ (x & z) ^ (y & z));
}
var Sigma0256 = function(x) {
return (S(x, 2) ^ S(x, 13) ^ S(x, 22));
}
var Sigma1256 = function(x) {
return (S(x, 6) ^ S(x, 11) ^ S(x, 25));
}
var Gamma0256 = function(x) {
return (S(x, 7) ^ S(x, 18) ^ R(x, 3));
}
var Gamma1256 = function (x) {
return (S(x, 17) ^ S(x, 19) ^ R(x, 10));
}
var core_sha256 = function(m, l) {
var K = new Array(0x428A2F98,0x71374491,0xB5C0FBCF,0xE9B5DBA5,0x3956C25B,0x59F111F1,0x923F82A4,0xAB1C5ED5,0xD807AA98,0x12835B01,0x243185BE,0x550C7DC3,0x72BE5D74,0x80DEB1FE,0x9BDC06A7,0xC19BF174,0xE49B69C1,0xEFBE4786,0xFC19DC6,0x240CA1CC,0x2DE92C6F,0x4A7484AA,0x5CB0A9DC,0x76F988DA,0x983E5152,0xA831C66D,0xB00327C8,0xBF597FC7,0xC6E00BF3,0xD5A79147,0x6CA6351,0x14292967,0x27B70A85,0x2E1B2138,0x4D2C6DFC,0x53380D13,0x650A7354,0x766A0ABB,0x81C2C92E,0x92722C85,0xA2BFE8A1,0xA81A664B,0xC24B8B70,0xC76C51A3,0xD192E819,0xD6990624,0xF40E3585,0x106AA070,0x19A4C116,0x1E376C08,0x2748774C,0x34B0BCB5,0x391C0CB3,0x4ED8AA4A,0x5B9CCA4F,0x682E6FF3,0x748F82EE,0x78A5636F,0x84C87814,0x8CC70208,0x90BEFFFA,0xA4506CEB,0xBEF9A3F7,0xC67178F2);
var HASH = new Array(0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19);
var W = new Array(64);
var a, b, c, d, e, f, g, h, i, j;
var T1, T2;
/* append padding */
m[l >> 5] |= 0x80 << (24 - l % 32);
m[((l + 64 >> 9) << 4) + 15] = l;
for ( var i = 0; i>5] |= (str.charCodeAt(i / chrsz) & mask) << (24 - i%32);
}
return bin;
}
var binb2hex = function(binarray) {
//var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */
//var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
var hex_tab = "0123456789abcdef";
var str = "";
for (var i = 0; i < binarray.length * 4; i++) {
str += hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8+4)) & 0xF) + hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8 )) & 0xF);
}
return str;
}
var core_hmac_sha256 = function(key, data) {
var bkey = str2binb(key);
if(bkey.length > 16) {
bkey = core_sha1(bkey, key.length * chrsz);
}
var ipad = Array(16), opad = Array(16);
for(var i = 0; i < 16; i++) {
ipad[i] = bkey[i] ^ 0x36363636;
opad[i] = bkey[i] ^ 0x5C5C5C5C;
}
var hash = core_sha256(ipad.concat(str2binb(data)), 512 + data.length * chrsz);
return core_sha256(opad.concat(hash), 512 + 256);
}
var prep = function(string){
string = typeof string == 'object' ? $(string).val() : string.toString();
return string;
}
// standard sha256 implementation: var x = $.sha256(value);
// standard sha266hmac implementation: varx = $.sha256hmac(value1, value2);
$.extend({
sha256 : function(string){
string = prep(string);
return binb2hex(core_sha256(str2binb(string),string.length * chrsz));
},
sha256hmac : function(key, data){
key = prep(key);
data = prep(data);
return binb2hex(core_hmac_sha256(key, data));
},
sha256config : function(bits){
chrsz = parseInt(bits) || 8;
}
});
// alternative sha256 implementation: var x = value.sha256();
$.fn.sha256 = function (bits) {
// change bits
$.sha256config(bits);
var string = prep($(this).val());
var val = $.sha256(string);
// reset bits, this was a one-time operation
$.sha256config(8);
return val;
};
})(jQuery);
/*
http://www.JSON.org/json2.js
2011-02-23
Public Domain.
NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
See http://www.JSON.org/js.html
This code should be minified before deployment.
See http://javascript.crockford.com/jsmin.html
USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
NOT CONTROL.
This file creates a global JSON object containing two methods: stringify
and parse.
JSON.stringify(value, replacer, space)
value any JavaScript value, usually an object or array.
replacer an optional parameter that determines how object
values are stringified for objects. It can be a
function or an array of strings.
space an optional parameter that specifies the indentation
of nested structures. If it is omitted, the text will
be packed without extra whitespace. If it is a number,
it will specify the number of spaces to indent at each
level. If it is a string (such as '\t' or ' '),
it contains the characters used to indent at each level.
This method produces a JSON text from a JavaScript value.
When an object value is found, if the object contains a toJSON
method, its toJSON method will be called and the result will be
stringified. A toJSON method does not serialize: it returns the
value represented by the name/value pair that should be serialized,
or undefined if nothing should be serialized. The toJSON method
will be passed the key associated with the value, and this will be
bound to the value
For example, this would serialize Dates as ISO strings.
Date.prototype.toJSON = function (key) {
function f(n) {
// Format integers to have at least two digits.
return n < 10 ? '0' + n : n;
}
return this.getUTCFullYear() + '-' +
f(this.getUTCMonth() + 1) + '-' +
f(this.getUTCDate()) + 'T' +
f(this.getUTCHours()) + ':' +
f(this.getUTCMinutes()) + ':' +
f(this.getUTCSeconds()) + 'Z';
};
You can provide an optional replacer method. It will be passed the
key and value of each member, with this bound to the containing
object. The value that is returned from your method will be
serialized. If your method returns undefined, then the member will
be excluded from the serialization.
If the replacer parameter is an array of strings, then it will be
used to select the members to be serialized. It filters the results
such that only members with keys listed in the replacer array are
stringified.
Values that do not have JSON representations, such as undefined or
functions, will not be serialized. Such values in objects will be
dropped; in arrays they will be replaced with null. You can use
a replacer function to replace those with JSON values.
JSON.stringify(undefined) returns undefined.
The optional space parameter produces a stringification of the
value that is filled with line breaks and indentation to make it
easier to read.
If the space parameter is a non-empty string, then that string will
be used for indentation. If the space parameter is a number, then
the indentation will be that many spaces.
Example:
text = JSON.stringify(['e', {pluribus: 'unum'}]);
// text is '["e",{"pluribus":"unum"}]'
text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
// text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
text = JSON.stringify([new Date()], function (key, value) {
return this[key] instanceof Date ?
'Date(' + this[key] + ')' : value;
});
// text is '["Date(---current time---)"]'
JSON.parse(text, reviver)
This method parses a JSON text to produce an object or array.
It can throw a SyntaxError exception.
The optional reviver parameter is a function that can filter and
transform the results. It receives each of the keys and values,
and its return value is used instead of the original value.
If it returns what it received, then the structure is not modified.
If it returns undefined then the member is deleted.
Example:
// Parse the text. Values that look like ISO date strings will
// be converted to Date objects.
myData = JSON.parse(text, function (key, value) {
var a;
if (typeof value === 'string') {
a =
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
if (a) {
return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
+a[5], +a[6]));
}
}
return value;
});
myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
var d;
if (typeof value === 'string' &&
value.slice(0, 5) === 'Date(' &&
value.slice(-1) === ')') {
d = new Date(value.slice(5, -1));
if (d) {
return d;
}
}
return value;
});
This is a reference implementation. You are free to copy, modify, or
redistribute.
*/
/*jslint evil: true, strict: false, regexp: false */
/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
lastIndex, length, parse, prototype, push, replace, slice, stringify,
test, toJSON, toString, valueOf
*/
// Create a JSON object only if one does not already exist. We create the
// methods in a closure to avoid creating global variables.
var JSON;
if (!JSON) {
JSON = {};
}
(function () {
"use strict";
function f(n) {
// Format integers to have at least two digits.
return n < 10 ? '0' + n : n;
}
if (typeof Date.prototype.toJSON !== 'function') {
Date.prototype.toJSON = function (key) {
return isFinite(this.valueOf()) ?
this.getUTCFullYear() + '-' +
f(this.getUTCMonth() + 1) + '-' +
f(this.getUTCDate()) + 'T' +
f(this.getUTCHours()) + ':' +
f(this.getUTCMinutes()) + ':' +
f(this.getUTCSeconds()) + 'Z' : null;
};
String.prototype.toJSON =
Number.prototype.toJSON =
Boolean.prototype.toJSON = function (key) {
return this.valueOf();
};
}
var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
gap,
indent,
meta = { // table of character substitutions
'\b': '\\b',
'\t': '\\t',
'\n': '\\n',
'\f': '\\f',
'\r': '\\r',
'"' : '\\"',
'\\': '\\\\'
},
rep;
function quote(string) {
// If the string contains no control characters, no quote characters, and no
// backslash characters, then we can safely slap some quotes around it.
// Otherwise we must also replace the offending characters with safe escape
// sequences.
escapable.lastIndex = 0;
return escapable.test(string) ? '"' + string.replace(escapable, function (a) {
var c = meta[a];
return typeof c === 'string' ? c :
'\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
}) + '"' : '"' + string + '"';
}
function str(key, holder) {
// Produce a string from holder[key].
var i, // The loop counter.
k, // The member key.
v, // The member value.
length,
mind = gap,
partial,
value = holder[key];
// If the value has a toJSON method, call it to obtain a replacement value.
if (value && typeof value === 'object' &&
typeof value.toJSON === 'function') {
value = value.toJSON(key);
}
// If we were called with a replacer function, then call the replacer to
// obtain a replacement value.
if (typeof rep === 'function') {
value = rep.call(holder, key, value);
}
// What happens next depends on the value's type.
switch (typeof value) {
case 'string':
return quote(value);
case 'number':
// JSON numbers must be finite. Encode non-finite numbers as null.
return isFinite(value) ? String(value) : 'null';
case 'boolean':
case 'null':
// If the value is a boolean or null, convert it to a string. Note:
// typeof null does not produce 'null'. The case is included here in
// the remote chance that this gets fixed someday.
return String(value);
// If the type is 'object', we might be dealing with an object or an array or
// null.
case 'object':
// Due to a specification blunder in ECMAScript, typeof null is 'object',
// so watch out for that case.
if (!value) {
return 'null';
}
// Make an array to hold the partial results of stringifying this object value.
gap += indent;
partial = [];
// Is the value an array?
if (Object.prototype.toString.apply(value) === '[object Array]') {
// The value is an array. Stringify every element. Use null as a placeholder
// for non-JSON values.
length = value.length;
for (i = 0; i < length; i += 1) {
partial[i] = str(i, value) || 'null';
}
// Join all of the elements together, separated with commas, and wrap them in
// brackets.
v = partial.length === 0 ? '[]' : gap ?
'[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' :
'[' + partial.join(',') + ']';
gap = mind;
return v;
}
// If the replacer is an array, use it to select the members to be stringified.
if (rep && typeof rep === 'object') {
length = rep.length;
for (i = 0; i < length; i += 1) {
if (typeof rep[i] === 'string') {
k = rep[i];
v = str(k, value);
if (v) {
partial.push(quote(k) + (gap ? ': ' : ':') + v);
}
}
}
} else {
// Otherwise, iterate through all of the keys in the object.
for (k in value) {
if (Object.prototype.hasOwnProperty.call(value, k)) {
v = str(k, value);
if (v) {
partial.push(quote(k) + (gap ? ': ' : ':') + v);
}
}
}
}
// Join all of the member texts together, separated with commas,
// and wrap them in braces.
v = partial.length === 0 ? '{}' : gap ?
'{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' :
'{' + partial.join(',') + '}';
gap = mind;
return v;
}
}
// If the JSON object does not yet have a stringify method, give it one.
if (typeof JSON.stringify !== 'function') {
JSON.stringify = function (value, replacer, space) {
// The stringify method takes a value and an optional replacer, and an optional
// space parameter, and returns a JSON text. The replacer can be a function
// that can replace values, or an array of strings that will select the keys.
// A default replacer method can be provided. Use of the space parameter can
// produce text that is more easily readable.
var i;
gap = '';
indent = '';
// If the space parameter is a number, make an indent string containing that
// many spaces.
if (typeof space === 'number') {
for (i = 0; i < space; i += 1) {
indent += ' ';
}
// If the space parameter is a string, it will be used as the indent string.
} else if (typeof space === 'string') {
indent = space;
}
// If there is a replacer, it must be a function or an array.
// Otherwise, throw an error.
rep = replacer;
if (replacer && typeof replacer !== 'function' &&
(typeof replacer !== 'object' ||
typeof replacer.length !== 'number')) {
throw new Error('JSON.stringify');
}
// Make a fake root object containing our value under the key of ''.
// Return the result of stringifying the value.
return str('', {'': value});
};
}
// If the JSON object does not yet have a parse method, give it one.
if (typeof JSON.parse !== 'function') {
JSON.parse = function (text, reviver) {
// The parse method takes a text and an optional reviver function, and returns
// a JavaScript value if the text is a valid JSON text.
var j;
function walk(holder, key) {
// The walk method is used to recursively walk the resulting structure so
// that modifications can be made.
var k, v, value = holder[key];
if (value && typeof value === 'object') {
for (k in value) {
if (Object.prototype.hasOwnProperty.call(value, k)) {
v = walk(value, k);
if (v !== undefined) {
value[k] = v;
} else {
delete value[k];
}
}
}
}
return reviver.call(holder, key, value);
}
// Parsing happens in four stages. In the first stage, we replace certain
// Unicode characters with escape sequences. JavaScript handles many characters
// incorrectly, either silently deleting them, or treating them as line endings.
text = String(text);
cx.lastIndex = 0;
if (cx.test(text)) {
text = text.replace(cx, function (a) {
return '\\u' +
('0000' + a.charCodeAt(0).toString(16)).slice(-4);
});
}
// In the second stage, we run the text against regular expressions that look
// for non-JSON patterns. We are especially concerned with '()' and 'new'
// because they can cause invocation, and '=' because it can cause mutation.
// But just to be safe, we want to reject all unexpected forms.
// We split the second stage into 4 regexp operations in order to work around
// crippling inefficiencies in IE's and Safari's regexp engines. First we
// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
// replace all simple value tokens with ']' characters. Third, we delete all
// open brackets that follow a colon or comma or that begin the text. Finally,
// we look to see that the remaining characters are only whitespace or ']' or
// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
if (/^[\],:{}\s]*$/
.test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@')
.replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']')
.replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
// In the third stage we use the eval function to compile the text into a
// JavaScript structure. The '{' operator is subject to a syntactic ambiguity
// in JavaScript: it can begin a block or an object literal. We wrap the text
// in parens to eliminate the ambiguity.
j = eval('(' + text + ')');
// In the optional fourth stage, we recursively walk the new structure, passing
// each name/value pair to a reviver function for possible transformation.
return typeof reviver === 'function' ?
walk({'': j}, '') : j;
}
// If the text is not JSON parseable, then a SyntaxError is thrown.
throw new SyntaxError('JSON.parse');
};
}
}());
/* =============================================================================
* FILE: jQueryTools-Overlay.js
* ========================================================================== */
(function($) {
// static constructs
$.tools = $.tools || {version: '1.2.5'};
$.tools.overlay = {
addEffect: function(name, loadFn, closeFn) {
effects[name] = [loadFn, closeFn];
},
conf: {
close: null,
closeOnClick: true,
closeOnEsc: false,
closeSpeed: 'fast',
effect: 'default',
// since 1.2. fixed positioning not supported by IE6 - which isn't supported - so remove unsupported check
fixed: true, //!$.browser.msie || $.browser.version > 6,
left: 'center',
load: false, // 1.2
mask: null,
oneInstance: true,
speed: 'normal',
target: null, // target element to be overlayed. by default taken from [rel]
top: '10%'
}
};
var instances = [], effects = {};
// the default effect. nice and easy!
$.tools.overlay.addEffect('default',
/*
onLoad/onClose functions must be called otherwise none of the
user supplied callback methods won't be called
*/
function(pos, onLoad) {
var conf = this.getConf(),
w = $(window);
if (!conf.fixed) {
pos.top += w.scrollTop();
pos.left += w.scrollLeft();
}
pos.position = conf.fixed ? 'fixed' : 'absolute';
this.getOverlay().css(pos).fadeIn(conf.speed, onLoad);
}, function(onClose) {
this.getOverlay().fadeOut(this.getConf().closeSpeed, onClose);
}
);
function Overlay(trigger, conf) {
// private variables
var self = this,
fire = trigger.add(self),
w = $(window),
closers,
overlay,
opened,
maskConf = $.tools.expose && (conf.mask || conf.expose),
uid = Math.random().toString().slice(10);
// mask configuration
if (maskConf) {
if (typeof maskConf == 'string') { maskConf = {color: maskConf}; }
maskConf.closeOnClick = maskConf.closeOnEsc = false;
}
// get overlay and triggerr
var jq = conf.target || trigger.attr("rel");
overlay = jq ? $(jq) : null || trigger;
// overlay not found. cannot continue
if (!overlay.length) { throw "Could not find Overlay: " + jq; }
// trigger's click event
if (trigger && trigger.index(overlay) == -1) {
trigger.click(function(e) {
self.load(e);
return e.preventDefault();
});
}
// API methods
$.extend(self, {
load: function(e) {
// can be opened only once
if (self.isOpened()) { return self; }
// find the effect
var eff = effects[conf.effect];
if (!eff) { throw "Overlay: cannot find effect : \"" + conf.effect + "\""; }
// close other instances?
if (conf.oneInstance) {
$.each(instances, function() {
this.close(e);
});
}
// onBeforeLoad
e = e || $.Event();
e.type = "onBeforeLoad";
fire.trigger(e);
if (e.isDefaultPrevented()) { return self; }
// opened
opened = true;
// possible mask effect
if (maskConf) { $(overlay).expose(maskConf); }
// position & dimensions
var top = conf.top,
left = conf.left,
oWidth = overlay.outerWidth({margin:true}),
oHeight = overlay.outerHeight({margin:true});
if (typeof top == 'string') {
top = top == 'center' ? Math.max((w.height() - oHeight) / 2, 0) :
parseInt(top, 10) / 100 * w.height();
}
if (left == 'center') { left = Math.max((w.width() - oWidth) / 2, 0); }
// load effect
eff[0].call(self, {top: top, left: left}, function() {
if (opened) {
e.type = "onLoad";
fire.trigger(e);
}
});
// mask.click closes overlay
if (maskConf && conf.closeOnClick) {
$.mask.getMask().one("click", self.close);
}
// when window is clicked outside overlay, we close
if (conf.closeOnClick) {
$(document).bind("click." + uid, function(e) {
if (!$(e.target).parents(overlay).length) {
self.close(e);
}
});
}
// keyboard::escape
if (conf.closeOnEsc) {
// one callback is enough if multiple instances are loaded simultaneously
$(document).bind("keydown." + uid, function(e) {
if (e.keyCode == 27) {
self.close(e);
}
});
}
return self;
},
close: function(e) {
if (!self.isOpened()) { return self; }
e = e || $.Event();
e.type = "onBeforeClose";
fire.trigger(e);
if (e.isDefaultPrevented()) { return; }
opened = false;
// close effect
effects[conf.effect][1].call(self, function() {
e.type = "onClose";
fire.trigger(e);
});
// unbind the keyboard / clicking actions
$(document).unbind("click." + uid).unbind("keydown." + uid);
if (maskConf) {
$.mask.close();
}
return self;
},
getOverlay: function() {
return overlay;
},
getTrigger: function() {
return trigger;
},
getClosers: function() {
return closers;
},
isOpened: function() {
return opened;
},
// manipulate start, finish and speeds
getConf: function() {
return conf;
}
});
// callbacks
$.each("onBeforeLoad,onStart,onLoad,onBeforeClose,onClose".split(","), function(i, name) {
// configuration
if ($.isFunction(conf[name])) {
$(self).bind(name, conf[name]);
}
// API
self[name] = function(fn) {
if (fn) { $(self).bind(name, fn); }
return self;
};
});
// close button
closers = overlay.find(conf.close || ".close");
if (!closers.length && !conf.close) {
closers = $('');
overlay.prepend(closers);
}
closers.click(function(e) {
self.close(e);
});
// autoload
if (conf.load) { self.load(); }
}
// jQuery plugin initialization
$.fn.overlay = function(conf) {
// already constructed --> return API
var el = this.data("overlay");
if (el) { return el; }
if ($.isFunction(conf)) {
conf = {onBeforeLoad: conf};
}
conf = $.extend(true, {}, $.tools.overlay.conf, conf);
this.each(function() {
el = new Overlay($(this), conf);
instances.push(el);
$(this).data("overlay", el);
});
return conf.api ? el: this;
};
})(jQuery);
/* =============================================================================
* FILE: jQueryTools-Expose.js
* ========================================================================== */
(function(b){function k(){if(BrowserDetect.browser === "Explorer"){var a=b(document).height(),d=b(window).height();return[window.innerWidth||document.documentElement.clientWidth||document.body.clientWidth,a-d<20?d:a]}return[b(document).width(),b(document).height()]}function h(a){if(a)return a.call(b.mask)}b.tools=b.tools||{version:"1.2.5"};var l;l=b.tools.expose={conf:{maskId:"exposeMask",loadSpeed:"slow",closeSpeed:"fast",closeOnClick:true,closeOnEsc:false,zIndex:9998,opacity:0.8,startOpacity:0,color:"#fff",onLoad:null,
onClose:null}};var c,i,e,g,j;b.mask={load:function(a,d){if(e)return this;if(typeof a=="string")a={color:a};a=a||g;g=a=b.extend(b.extend({},l.conf),a);c=b("#"+a.maskId);if(!c.length){c=b("").attr("id",a.maskId);b("body").append(c)}var m=k();c.css({position:"absolute",top:0,left:0,width:m[0],height:m[1],display:"none",opacity:a.startOpacity,zIndex:a.zIndex});a.color&&c.css("backgroundColor",a.color);if(h(a.onBeforeLoad)===false)return this;a.closeOnEsc&&b(document).bind("keydown.mask",function(f){f.keyCode==
27&&b.mask.close(f)});a.closeOnClick&&c.bind("click.mask",function(f){b.mask.close(f)});b(window).bind("resize.mask",function(){b.mask.fit()});if(d&&d.length){j=d.eq(0).css("zIndex");b.each(d,function(){var f=b(this);/relative|absolute|fixed/i.test(f.css("position"))||f.css("position","relative")});i=d.css({zIndex:Math.max(a.zIndex+1,j=="auto"?0:j)})}c.css({display:"block"}).fadeTo(a.loadSpeed,a.opacity,function(){b.mask.fit();h(a.onLoad);e="full"});e=true;return this},close:function(){if(e){if(h(g.onBeforeClose)===
false)return this;c.fadeOut(g.closeSpeed,function(){h(g.onClose);i&&i.css({zIndex:j});e=false});b(document).unbind("keydown.mask");c.unbind("click.mask");b(window).unbind("resize.mask")}return this},fit:function(){if(e){var a=k();c.css({width:a[0],height:a[1]})}},getMask:function(){return c},isLoaded:function(a){return a?e=="full":e},getConf:function(){return g},getExposed:function(){return i}};b.fn.mask=function(a){b.mask.load(a);return this};b.fn.expose=function(a){b.mask.load(a,this);return this}})(jQuery);
// Underscore.js 1.3.1
// (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc.
// Underscore is freely distributable under the MIT license.
// Portions of Underscore are inspired or borrowed from Prototype,
// Oliver Steele's Functional, and John Resig's Micro-Templating.
// For all details and documentation:
// http://documentcloud.github.com/underscore
(function() {
// Baseline setup
// --------------
// Establish the root object, `window` in the browser, or `global` on the server.
var root = this;
// Save the previous value of the `_` variable.
var previousUnderscore = root._;
// Establish the object that gets returned to break out of a loop iteration.
var breaker = {};
// Save bytes in the minified (but not gzipped) version:
var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;
// Create quick reference variables for speed access to core prototypes.
var slice = ArrayProto.slice,
unshift = ArrayProto.unshift,
toString = ObjProto.toString,
hasOwnProperty = ObjProto.hasOwnProperty;
// All **ECMAScript 5** native function implementations that we hope to use
// are declared here.
var
nativeForEach = ArrayProto.forEach,
nativeMap = ArrayProto.map,
nativeReduce = ArrayProto.reduce,
nativeReduceRight = ArrayProto.reduceRight,
nativeFilter = ArrayProto.filter,
nativeEvery = ArrayProto.every,
nativeSome = ArrayProto.some,
nativeIndexOf = ArrayProto.indexOf,
nativeLastIndexOf = ArrayProto.lastIndexOf,
nativeIsArray = Array.isArray,
nativeKeys = Object.keys,
nativeBind = FuncProto.bind;
// Create a safe reference to the Underscore object for use below.
var _ = function(obj) { return new wrapper(obj); };
// Export the Underscore object for **Node.js**, with
// backwards-compatibility for the old `require()` API. If we're in
// the browser, add `_` as a global object via a string identifier,
// for Closure Compiler "advanced" mode.
if (typeof exports !== 'undefined') {
if (typeof module !== 'undefined' && module.exports) {
exports = module.exports = _;
}
exports._ = _;
} else {
root['_'] = _;
}
// Current version.
_.VERSION = '1.3.1';
// Collection Functions
// --------------------
// The cornerstone, an `each` implementation, aka `forEach`.
// Handles objects with the built-in `forEach`, arrays, and raw objects.
// Delegates to **ECMAScript 5**'s native `forEach` if available.
var each = _.each = _.forEach = function(obj, iterator, context) {
if (obj == null) return;
if (nativeForEach && obj.forEach === nativeForEach) {
obj.forEach(iterator, context);
} else if (obj.length === +obj.length) {
for (var i = 0, l = obj.length; i < l; i++) {
if (i in obj && iterator.call(context, obj[i], i, obj) === breaker) return;
}
} else {
for (var key in obj) {
if (_.has(obj, key)) {
if (iterator.call(context, obj[key], key, obj) === breaker) return;
}
}
}
};
// Return the results of applying the iterator to each element.
// Delegates to **ECMAScript 5**'s native `map` if available.
_.map = _.collect = function(obj, iterator, context) {
var results = [];
if (obj == null) return results;
if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context);
each(obj, function(value, index, list) {
results[results.length] = iterator.call(context, value, index, list);
});
if (obj.length === +obj.length) results.length = obj.length;
return results;
};
// **Reduce** builds up a single result from a list of values, aka `inject`,
// or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available.
_.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) {
var initial = arguments.length > 2;
if (obj == null) obj = [];
if (nativeReduce && obj.reduce === nativeReduce) {
if (context) iterator = _.bind(iterator, context);
return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator);
}
each(obj, function(value, index, list) {
if (!initial) {
memo = value;
initial = true;
} else {
memo = iterator.call(context, memo, value, index, list);
}
});
if (!initial) throw new TypeError('Reduce of empty array with no initial value');
return memo;
};
// The right-associative version of reduce, also known as `foldr`.
// Delegates to **ECMAScript 5**'s native `reduceRight` if available.
_.reduceRight = _.foldr = function(obj, iterator, memo, context) {
var initial = arguments.length > 2;
if (obj == null) obj = [];
if (nativeReduceRight && obj.reduceRight === nativeReduceRight) {
if (context) iterator = _.bind(iterator, context);
return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator);
}
var reversed = _.toArray(obj).reverse();
if (context && !initial) iterator = _.bind(iterator, context);
return initial ? _.reduce(reversed, iterator, memo, context) : _.reduce(reversed, iterator);
};
// Return the first value which passes a truth test. Aliased as `detect`.
_.find = _.detect = function(obj, iterator, context) {
var result;
any(obj, function(value, index, list) {
if (iterator.call(context, value, index, list)) {
result = value;
return true;
}
});
return result;
};
// Return all the elements that pass a truth test.
// Delegates to **ECMAScript 5**'s native `filter` if available.
// Aliased as `select`.
_.filter = _.select = function(obj, iterator, context) {
var results = [];
if (obj == null) return results;
if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context);
each(obj, function(value, index, list) {
if (iterator.call(context, value, index, list)) results[results.length] = value;
});
return results;
};
// Return all the elements for which a truth test fails.
_.reject = function(obj, iterator, context) {
var results = [];
if (obj == null) return results;
each(obj, function(value, index, list) {
if (!iterator.call(context, value, index, list)) results[results.length] = value;
});
return results;
};
// Determine whether all of the elements match a truth test.
// Delegates to **ECMAScript 5**'s native `every` if available.
// Aliased as `all`.
_.every = _.all = function(obj, iterator, context) {
var result = true;
if (obj == null) return result;
if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context);
each(obj, function(value, index, list) {
if (!(result = result && iterator.call(context, value, index, list))) return breaker;
});
return result;
};
// Determine if at least one element in the object matches a truth test.
// Delegates to **ECMAScript 5**'s native `some` if available.
// Aliased as `any`.
var any = _.some = _.any = function(obj, iterator, context) {
iterator || (iterator = _.identity);
var result = false;
if (obj == null) return result;
if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context);
each(obj, function(value, index, list) {
if (result || (result = iterator.call(context, value, index, list))) return breaker;
});
return !!result;
};
// Determine if a given value is included in the array or object using `===`.
// Aliased as `contains`.
_.include = _.contains = function(obj, target) {
var found = false;
if (obj == null) return found;
if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1;
found = any(obj, function(value) {
return value === target;
});
return found;
};
// Invoke a method (with arguments) on every item in a collection.
_.invoke = function(obj, method) {
var args = slice.call(arguments, 2);
return _.map(obj, function(value) {
return (_.isFunction(method) ? method || value : value[method]).apply(value, args);
});
};
// Convenience version of a common use case of `map`: fetching a property.
_.pluck = function(obj, key) {
return _.map(obj, function(value){ return value[key]; });
};
// Return the maximum element or (element-based computation).
_.max = function(obj, iterator, context) {
if (!iterator && _.isArray(obj)) return Math.max.apply(Math, obj);
if (!iterator && _.isEmpty(obj)) return -Infinity;
var result = {computed : -Infinity};
each(obj, function(value, index, list) {
var computed = iterator ? iterator.call(context, value, index, list) : value;
computed >= result.computed && (result = {value : value, computed : computed});
});
return result.value;
};
// Return the minimum element (or element-based computation).
_.min = function(obj, iterator, context) {
if (!iterator && _.isArray(obj)) return Math.min.apply(Math, obj);
if (!iterator && _.isEmpty(obj)) return Infinity;
var result = {computed : Infinity};
each(obj, function(value, index, list) {
var computed = iterator ? iterator.call(context, value, index, list) : value;
computed < result.computed && (result = {value : value, computed : computed});
});
return result.value;
};
// Shuffle an array.
_.shuffle = function(obj) {
var shuffled = [], rand;
each(obj, function(value, index, list) {
if (index == 0) {
shuffled[0] = value;
} else {
rand = Math.floor(Math.random() * (index + 1));
shuffled[index] = shuffled[rand];
shuffled[rand] = value;
}
});
return shuffled;
};
// Sort the object's values by a criterion produced by an iterator.
_.sortBy = function(obj, iterator, context) {
return _.pluck(_.map(obj, function(value, index, list) {
return {
value : value,
criteria : iterator.call(context, value, index, list)
};
}).sort(function(left, right) {
var a = left.criteria, b = right.criteria;
return a < b ? -1 : a > b ? 1 : 0;
}), 'value');
};
// Groups the object's values by a criterion. Pass either a string attribute
// to group by, or a function that returns the criterion.
_.groupBy = function(obj, val) {
var result = {};
var iterator = _.isFunction(val) ? val : function(obj) { return obj[val]; };
each(obj, function(value, index) {
var key = iterator(value, index);
(result[key] || (result[key] = [])).push(value);
});
return result;
};
// Use a comparator function to figure out at what index an object should
// be inserted so as to maintain order. Uses binary search.
_.sortedIndex = function(array, obj, iterator) {
iterator || (iterator = _.identity);
var low = 0, high = array.length;
while (low < high) {
var mid = (low + high) >> 1;
iterator(array[mid]) < iterator(obj) ? low = mid + 1 : high = mid;
}
return low;
};
// Safely convert anything iterable into a real, live array.
_.toArray = function(iterable) {
if (!iterable) return [];
if (iterable.toArray) return iterable.toArray();
if (_.isArray(iterable)) return slice.call(iterable);
if (_.isArguments(iterable)) return slice.call(iterable);
return _.values(iterable);
};
// Return the number of elements in an object.
_.size = function(obj) {
return _.toArray(obj).length;
};
// Array Functions
// ---------------
// Get the first element of an array. Passing **n** will return the first N
// values in the array. Aliased as `head`. The **guard** check allows it to work
// with `_.map`.
_.first = _.head = function(array, n, guard) {
return (n != null) && !guard ? slice.call(array, 0, n) : array[0];
};
// Returns everything but the last entry of the array. Especcialy useful on
// the arguments object. Passing **n** will return all the values in
// the array, excluding the last N. The **guard** check allows it to work with
// `_.map`.
_.initial = function(array, n, guard) {
return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n));
};
// Get the last element of an array. Passing **n** will return the last N
// values in the array. The **guard** check allows it to work with `_.map`.
_.last = function(array, n, guard) {
if ((n != null) && !guard) {
return slice.call(array, Math.max(array.length - n, 0));
} else {
return array[array.length - 1];
}
};
// Returns everything but the first entry of the array. Aliased as `tail`.
// Especially useful on the arguments object. Passing an **index** will return
// the rest of the values in the array from that index onward. The **guard**
// check allows it to work with `_.map`.
_.rest = _.tail = function(array, index, guard) {
return slice.call(array, (index == null) || guard ? 1 : index);
};
// Trim out all falsy values from an array.
_.compact = function(array) {
return _.filter(array, function(value){ return !!value; });
};
// Return a completely flattened version of an array.
_.flatten = function(array, shallow) {
return _.reduce(array, function(memo, value) {
if (_.isArray(value)) return memo.concat(shallow ? value : _.flatten(value));
memo[memo.length] = value;
return memo;
}, []);
};
// Return a version of the array that does not contain the specified value(s).
_.without = function(array) {
return _.difference(array, slice.call(arguments, 1));
};
// Produce a duplicate-free version of the array. If the array has already
// been sorted, you have the option of using a faster algorithm.
// Aliased as `unique`.
_.uniq = _.unique = function(array, isSorted, iterator) {
var initial = iterator ? _.map(array, iterator) : array;
var result = [];
_.reduce(initial, function(memo, el, i) {
if (0 == i || (isSorted === true ? _.last(memo) != el : !_.include(memo, el))) {
memo[memo.length] = el;
result[result.length] = array[i];
}
return memo;
}, []);
return result;
};
// Produce an array that contains the union: each distinct element from all of
// the passed-in arrays.
_.union = function() {
return _.uniq(_.flatten(arguments, true));
};
// Produce an array that contains every item shared between all the
// passed-in arrays. (Aliased as "intersect" for back-compat.)
_.intersection = _.intersect = function(array) {
var rest = slice.call(arguments, 1);
return _.filter(_.uniq(array), function(item) {
return _.every(rest, function(other) {
return _.indexOf(other, item) >= 0;
});
});
};
// Take the difference between one array and a number of other arrays.
// Only the elements present in just the first array will remain.
_.difference = function(array) {
var rest = _.flatten(slice.call(arguments, 1));
return _.filter(array, function(value){ return !_.include(rest, value); });
};
// Zip together multiple lists into a single array -- elements that share
// an index go together.
_.zip = function() {
var args = slice.call(arguments);
var length = _.max(_.pluck(args, 'length'));
var results = new Array(length);
for (var i = 0; i < length; i++) results[i] = _.pluck(args, "" + i);
return results;
};
// If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**),
// we need this function. Return the position of the first occurrence of an
// item in an array, or -1 if the item is not included in the array.
// Delegates to **ECMAScript 5**'s native `indexOf` if available.
// If the array is large and already in sort order, pass `true`
// for **isSorted** to use binary search.
_.indexOf = function(array, item, isSorted) {
if (array == null) return -1;
var i, l;
if (isSorted) {
i = _.sortedIndex(array, item);
return array[i] === item ? i : -1;
}
if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item);
for (i = 0, l = array.length; i < l; i++) if (i in array && array[i] === item) return i;
return -1;
};
// Delegates to **ECMAScript 5**'s native `lastIndexOf` if available.
_.lastIndexOf = function(array, item) {
if (array == null) return -1;
if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) return array.lastIndexOf(item);
var i = array.length;
while (i--) if (i in array && array[i] === item) return i;
return -1;
};
// Generate an integer Array containing an arithmetic progression. A port of
// the native Python `range()` function. See
// [the Python documentation](http://docs.python.org/library/functions.html#range).
_.range = function(start, stop, step) {
if (arguments.length <= 1) {
stop = start || 0;
start = 0;
}
step = arguments[2] || 1;
var len = Math.max(Math.ceil((stop - start) / step), 0);
var idx = 0;
var range = new Array(len);
while(idx < len) {
range[idx++] = start;
start += step;
}
return range;
};
// Function (ahem) Functions
// ------------------
// Reusable constructor function for prototype setting.
var ctor = function(){};
// Create a function bound to a given object (assigning `this`, and arguments,
// optionally). Binding with arguments is also known as `curry`.
// Delegates to **ECMAScript 5**'s native `Function.bind` if available.
// We check for `func.bind` first, to fail fast when `func` is undefined.
_.bind = function bind(func, context) {
var bound, args;
if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
if (!_.isFunction(func)) throw new TypeError;
args = slice.call(arguments, 2);
return bound = function() {
if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments)));
ctor.prototype = func.prototype;
var self = new ctor;
var result = func.apply(self, args.concat(slice.call(arguments)));
if (Object(result) === result) return result;
return self;
};
};
// Bind all of an object's methods to that object. Useful for ensuring that
// all callbacks defined on an object belong to it.
_.bindAll = function(obj) {
var funcs = slice.call(arguments, 1);
if (funcs.length == 0) funcs = _.functions(obj);
each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); });
return obj;
};
// Memoize an expensive function by storing its results.
_.memoize = function(func, hasher) {
var memo = {};
hasher || (hasher = _.identity);
return function() {
var key = hasher.apply(this, arguments);
return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments));
};
};
// Delays a function for the given number of milliseconds, and then calls
// it with the arguments supplied.
_.delay = function(func, wait) {
var args = slice.call(arguments, 2);
return setTimeout(function(){ return func.apply(func, args); }, wait);
};
// Defers a function, scheduling it to run after the current call stack has
// cleared.
_.defer = function(func) {
return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1)));
};
// Returns a function, that, when invoked, will only be triggered at most once
// during a given window of time.
_.throttle = function(func, wait) {
var context, args, timeout, throttling, more;
var whenDone = _.debounce(function(){ more = throttling = false; }, wait);
return function() {
context = this; args = arguments;
var later = function() {
timeout = null;
if (more) func.apply(context, args);
whenDone();
};
if (!timeout) timeout = setTimeout(later, wait);
if (throttling) {
more = true;
} else {
func.apply(context, args);
}
whenDone();
throttling = true;
};
};
// Returns a function, that, as long as it continues to be invoked, will not
// be triggered. The function will be called after it stops being called for
// N milliseconds.
_.debounce = function(func, wait) {
var timeout;
return function() {
var context = this, args = arguments;
var later = function() {
timeout = null;
func.apply(context, args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
};
// Returns a function that will be executed at most one time, no matter how
// often you call it. Useful for lazy initialization.
_.once = function(func) {
var ran = false, memo;
return function() {
if (ran) return memo;
ran = true;
return memo = func.apply(this, arguments);
};
};
// Returns the first function passed as an argument to the second,
// allowing you to adjust arguments, run code before and after, and
// conditionally execute the original function.
_.wrap = function(func, wrapper) {
return function() {
var args = [func].concat(slice.call(arguments, 0));
return wrapper.apply(this, args);
};
};
// Returns a function that is the composition of a list of functions, each
// consuming the return value of the function that follows.
_.compose = function() {
var funcs = arguments;
return function() {
var args = arguments;
for (var i = funcs.length - 1; i >= 0; i--) {
args = [funcs[i].apply(this, args)];
}
return args[0];
};
};
// Returns a function that will only be executed after being called N times.
_.after = function(times, func) {
if (times <= 0) return func();
return function() {
if (--times < 1) { return func.apply(this, arguments); }
};
};
// Object Functions
// ----------------
// Retrieve the names of an object's properties.
// Delegates to **ECMAScript 5**'s native `Object.keys`
_.keys = nativeKeys || function(obj) {
if (obj !== Object(obj)) throw new TypeError('Invalid object');
var keys = [];
for (var key in obj) if (_.has(obj, key)) keys[keys.length] = key;
return keys;
};
// Retrieve the values of an object's properties.
_.values = function(obj) {
return _.map(obj, _.identity);
};
// Return a sorted list of the function names available on the object.
// Aliased as `methods`
_.functions = _.methods = function(obj) {
var names = [];
for (var key in obj) {
if (_.isFunction(obj[key])) names.push(key);
}
return names.sort();
};
// Extend a given object with all the properties in passed-in object(s).
_.extend = function(obj) {
each(slice.call(arguments, 1), function(source) {
for (var prop in source) {
obj[prop] = source[prop];
}
});
return obj;
};
// Fill in a given object with default properties.
_.defaults = function(obj) {
each(slice.call(arguments, 1), function(source) {
for (var prop in source) {
if (obj[prop] == null) obj[prop] = source[prop];
}
});
return obj;
};
// Create a (shallow-cloned) duplicate of an object.
_.clone = function(obj) {
if (!_.isObject(obj)) return obj;
return _.isArray(obj) ? obj.slice() : _.extend({}, obj);
};
// Invokes interceptor with the obj, and then returns obj.
// The primary purpose of this method is to "tap into" a method chain, in
// order to perform operations on intermediate results within the chain.
_.tap = function(obj, interceptor) {
interceptor(obj);
return obj;
};
// Internal recursive comparison function.
function eq(a, b, stack) {
// Identical objects are equal. `0 === -0`, but they aren't identical.
// See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal.
if (a === b) return a !== 0 || 1 / a == 1 / b;
// A strict comparison is necessary because `null == undefined`.
if (a == null || b == null) return a === b;
// Unwrap any wrapped objects.
if (a._chain) a = a._wrapped;
if (b._chain) b = b._wrapped;
// Invoke a custom `isEqual` method if one is provided.
if (a.isEqual && _.isFunction(a.isEqual)) return a.isEqual(b);
if (b.isEqual && _.isFunction(b.isEqual)) return b.isEqual(a);
// Compare `[[Class]]` names.
var className = toString.call(a);
if (className != toString.call(b)) return false;
switch (className) {
// Strings, numbers, dates, and booleans are compared by value.
case '[object String]':
// Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
// equivalent to `new String("5")`.
return a == String(b);
case '[object Number]':
// `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for
// other numeric values.
return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b);
case '[object Date]':
case '[object Boolean]':
// Coerce dates and booleans to numeric primitive values. Dates are compared by their
// millisecond representations. Note that invalid dates with millisecond representations
// of `NaN` are not equivalent.
return +a == +b;
// RegExps are compared by their source patterns and flags.
case '[object RegExp]':
return a.source == b.source &&
a.global == b.global &&
a.multiline == b.multiline &&
a.ignoreCase == b.ignoreCase;
}
if (typeof a != 'object' || typeof b != 'object') return false;
// Assume equality for cyclic structures. The algorithm for detecting cyclic
// structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.
var length = stack.length;
while (length--) {
// Linear search. Performance is inversely proportional to the number of
// unique nested structures.
if (stack[length] == a) return true;
}
// Add the first object to the stack of traversed objects.
stack.push(a);
var size = 0, result = true;
// Recursively compare objects and arrays.
if (className == '[object Array]') {
// Compare array lengths to determine if a deep comparison is necessary.
size = a.length;
result = size == b.length;
if (result) {
// Deep compare the contents, ignoring non-numeric properties.
while (size--) {
// Ensure commutative equality for sparse arrays.
if (!(result = size in a == size in b && eq(a[size], b[size], stack))) break;
}
}
} else {
// Objects with different constructors are not equivalent.
if ('constructor' in a != 'constructor' in b || a.constructor != b.constructor) return false;
// Deep compare objects.
for (var key in a) {
if (_.has(a, key)) {
// Count the expected number of properties.
size++;
// Deep compare each member.
if (!(result = _.has(b, key) && eq(a[key], b[key], stack))) break;
}
}
// Ensure that both objects contain the same number of properties.
if (result) {
for (key in b) {
if (_.has(b, key) && !(size--)) break;
}
result = !size;
}
}
// Remove the first object from the stack of traversed objects.
stack.pop();
return result;
}
// Perform a deep comparison to check if two objects are equal.
_.isEqual = function(a, b) {
return eq(a, b, []);
};
// Is a given array, string, or object empty?
// An "empty" object has no enumerable own-properties.
_.isEmpty = function(obj) {
if (_.isArray(obj) || _.isString(obj)) return obj.length === 0;
for (var key in obj) if (_.has(obj, key)) return false;
return true;
};
// Is a given value a DOM element?
_.isElement = function(obj) {
return !!(obj && obj.nodeType == 1);
};
// Is a given value an array?
// Delegates to ECMA5's native Array.isArray
_.isArray = nativeIsArray || function(obj) {
return toString.call(obj) == '[object Array]';
};
// Is a given variable an object?
_.isObject = function(obj) {
return obj === Object(obj);
};
// Is a given variable an arguments object?
_.isArguments = function(obj) {
return toString.call(obj) == '[object Arguments]';
};
if (!_.isArguments(arguments)) {
_.isArguments = function(obj) {
return !!(obj && _.has(obj, 'callee'));
};
}
// Is a given value a function?
_.isFunction = function(obj) {
return toString.call(obj) == '[object Function]';
};
// Is a given value a string?
_.isString = function(obj) {
return toString.call(obj) == '[object String]';
};
// Is a given value a number?
_.isNumber = function(obj) {
return toString.call(obj) == '[object Number]';
};
// Is the given value `NaN`?
_.isNaN = function(obj) {
// `NaN` is the only value for which `===` is not reflexive.
return obj !== obj;
};
// Is a given value a boolean?
_.isBoolean = function(obj) {
return obj === true || obj === false || toString.call(obj) == '[object Boolean]';
};
// Is a given value a date?
_.isDate = function(obj) {
return toString.call(obj) == '[object Date]';
};
// Is the given value a regular expression?
_.isRegExp = function(obj) {
return toString.call(obj) == '[object RegExp]';
};
// Is a given value equal to null?
_.isNull = function(obj) {
return obj === null;
};
// Is a given variable undefined?
_.isUndefined = function(obj) {
return obj === void 0;
};
// Has own property?
_.has = function(obj, key) {
return hasOwnProperty.call(obj, key);
};
// Utility Functions
// -----------------
// Run Underscore.js in *noConflict* mode, returning the `_` variable to its
// previous owner. Returns a reference to the Underscore object.
_.noConflict = function() {
root._ = previousUnderscore;
return this;
};
// Keep the identity function around for default iterators.
_.identity = function(value) {
return value;
};
// Run a function **n** times.
_.times = function (n, iterator, context) {
for (var i = 0; i < n; i++) iterator.call(context, i);
};
// Escape a string for HTML interpolation.
_.escape = function(string) {
return (''+string).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''').replace(/\//g,'/');
};
// Add your own custom functions to the Underscore object, ensuring that
// they're correctly added to the OOP wrapper as well.
_.mixin = function(obj) {
each(_.functions(obj), function(name){
addToWrapper(name, _[name] = obj[name]);
});
};
// Generate a unique integer id (unique within the entire client session).
// Useful for temporary DOM ids.
var idCounter = 0;
_.uniqueId = function(prefix) {
var id = idCounter++;
return prefix ? prefix + id : id;
};
// By default, Underscore uses ERB-style template delimiters, change the
// following template settings to use alternative delimiters.
_.templateSettings = {
evaluate : /<@([\s\S]+?)@>/g,
interpolate : /<@=([\s\S]+?)@>/g,
escape : /<@-([\s\S]+?)@>/g
};
// When customizing `templateSettings`, if you don't want to define an
// interpolation, evaluation or escaping regex, we need one that is
// guaranteed not to match.
var noMatch = /.^/;
// Within an interpolation, evaluation, or escaping, remove HTML escaping
// that had been previously added.
var unescape = function(code) {
return code.replace(/\\\\/g, '\\').replace(/\\'/g, "'");
};
// JavaScript micro-templating, similar to John Resig's implementation.
// Underscore templating handles arbitrary delimiters, preserves whitespace,
// and correctly escapes quotes within interpolated code.
_.template = function(str, data) {
var c = _.templateSettings;
var tmpl = 'var __p=[],print=function(){__p.push.apply(__p,arguments);};' +
'with(obj||{}){__p.push(\'' +
str.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'")
.replace(c.escape || noMatch, function(match, code) {
return "',_.escape(" + unescape(code) + "),'";
})
.replace(c.interpolate || noMatch, function(match, code) {
return "'," + unescape(code) + ",'";
})
.replace(c.evaluate || noMatch, function(match, code) {
return "');" + unescape(code).replace(/[\r\n\t]/g, ' ') + ";__p.push('";
})
.replace(/\r/g, '\\r')
.replace(/\n/g, '\\n')
.replace(/\t/g, '\\t')
+ "');}return __p.join('');";
var func = new Function('obj', '_', tmpl);
if (data) return func(data, _);
return function(data) {
return func.call(this, data, _);
};
};
// Add a "chain" function, which will delegate to the wrapper.
_.chain = function(obj) {
return _(obj).chain();
};
// The OOP Wrapper
// ---------------
// If Underscore is called as a function, it returns a wrapped object that
// can be used OO-style. This wrapper holds altered versions of all the
// underscore functions. Wrapped objects may be chained.
var wrapper = function(obj) { this._wrapped = obj; };
// Expose `wrapper.prototype` as `_.prototype`
_.prototype = wrapper.prototype;
// Helper function to continue chaining intermediate results.
var result = function(obj, chain) {
return chain ? _(obj).chain() : obj;
};
// A method to easily add functions to the OOP wrapper.
var addToWrapper = function(name, func) {
wrapper.prototype[name] = function() {
var args = slice.call(arguments);
unshift.call(args, this._wrapped);
return result(func.apply(_, args), this._chain);
};
};
// Add all of the Underscore functions to the wrapper object.
_.mixin(_);
// Add all mutator Array functions to the wrapper.
each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
var method = ArrayProto[name];
wrapper.prototype[name] = function() {
var wrapped = this._wrapped;
method.apply(wrapped, arguments);
var length = wrapped.length;
if ((name == 'shift' || name == 'splice') && length === 0) delete wrapped[0];
return result(wrapped, this._chain);
};
});
// Add all accessor Array functions to the wrapper.
each(['concat', 'join', 'slice'], function(name) {
var method = ArrayProto[name];
wrapper.prototype[name] = function() {
return result(method.apply(this._wrapped, arguments), this._chain);
};
});
// Start chaining a wrapped Underscore object.
wrapper.prototype.chain = function() {
this._chain = true;
return this;
};
// Extracts the result from a wrapped and chained object.
wrapper.prototype.value = function() {
return this._wrapped;
};
}).call(this);
// Backbone.js 0.9.2
// (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.
// Backbone may be freely distributed under the MIT license.
// For all details and documentation:
// http://backbonejs.org
(function(){
// Initial Setup
// -------------
// Save a reference to the global object (`window` in the browser, `global`
// on the server).
var root = this;
// Save the previous value of the `Backbone` variable, so that it can be
// restored later on, if `noConflict` is used.
var previousBackbone = root.Backbone;
// Create a local reference to slice/splice.
var slice = Array.prototype.slice;
var splice = Array.prototype.splice;
// The top-level namespace. All public Backbone classes and modules will
// be attached to this. Exported for both CommonJS and the browser.
var Backbone;
if (typeof exports !== 'undefined') {
Backbone = exports;
} else {
Backbone = root.Backbone = {};
}
// Current version of the library. Keep in sync with `package.json`.
Backbone.VERSION = '0.9.2';
// Require Underscore, if we're on the server, and it's not already present.
var _ = root._;
if (!_ && (typeof require !== 'undefined')) _ = require('underscore');
// For Backbone's purposes, jQuery, Zepto, or Ender owns the `$` variable.
var $ = root.jQuery || root.Zepto || root.ender;
// Set the JavaScript library that will be used for DOM manipulation and
// Ajax calls (a.k.a. the `$` variable). By default Backbone will use: jQuery,
// Zepto, or Ender; but the `setDomLibrary()` method lets you inject an
// alternate JavaScript library (or a mock library for testing your views
// outside of a browser).
Backbone.setDomLibrary = function(lib) {
$ = lib;
};
// Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable
// to its previous owner. Returns a reference to this Backbone object.
Backbone.noConflict = function() {
root.Backbone = previousBackbone;
return this;
};
// Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option
// will fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and
// set a `X-Http-Method-Override` header.
Backbone.emulateHTTP = false;
// Turn on `emulateJSON` to support legacy servers that can't deal with direct
// `application/json` requests ... will encode the body as
// `application/x-www-form-urlencoded` instead and will send the model in a
// form param named `model`.
Backbone.emulateJSON = false;
// Backbone.Events
// -----------------
// Regular expression used to split event strings
var eventSplitter = /\s+/;
// A module that can be mixed in to *any object* in order to provide it with
// custom events. You may bind with `on` or remove with `off` callback functions
// to an event; trigger`-ing an event fires all callbacks in succession.
//
// var object = {};
// _.extend(object, Backbone.Events);
// object.on('expand', function(){ alert('expanded'); });
// object.trigger('expand');
//
var Events = Backbone.Events = {
// Bind one or more space separated events, `events`, to a `callback`
// function. Passing `"all"` will bind the callback to all events fired.
on: function(events, callback, context) {
var calls, event, node, tail, list;
if (!callback) return this;
events = events.split(eventSplitter);
calls = this._callbacks || (this._callbacks = {});
// Create an immutable callback list, allowing traversal during
// modification. The tail is an empty object that will always be used
// as the next node.
while (event = events.shift()) {
list = calls[event];
node = list ? list.tail : {};
node.next = tail = {};
node.context = context;
node.callback = callback;
calls[event] = {tail: tail, next: list ? list.next : node};
}
return this;
},
// Remove one or many callbacks. If `context` is null, removes all callbacks
// with that function. If `callback` is null, removes all callbacks for the
// event. If `events` is null, removes all bound callbacks for all events.
off: function(events, callback, context) {
var event, calls, node, tail, cb, ctx;
// No events, or removing *all* events.
if (!(calls = this._callbacks)) return;
if (!(events || callback || context)) {
delete this._callbacks;
return this;
}
// Loop through the listed events and contexts, splicing them out of the
// linked list of callbacks if appropriate.
events = events ? events.split(eventSplitter) : _.keys(calls);
while (event = events.shift()) {
node = calls[event];
delete calls[event];
if (!node || !(callback || context)) continue;
// Create a new list, omitting the indicated callbacks.
tail = node.tail;
while ((node = node.next) !== tail) {
cb = node.callback;
ctx = node.context;
if ((callback && cb !== callback) || (context && ctx !== context)) {
this.on(event, cb, ctx);
}
}
}
return this;
},
// Trigger one or many events, firing all bound callbacks. Callbacks are
// passed the same arguments as `trigger` is, apart from the event name
// (unless you're listening on `"all"`, which will cause your callback to
// receive the true name of the event as the first argument).
trigger: function(events) {
var event, node, calls, tail, args, all, rest;
if (!(calls = this._callbacks)) return this;
all = calls.all;
events = events.split(eventSplitter);
rest = slice.call(arguments, 1);
// For each event, walk through the linked list of callbacks twice,
// first to trigger the event, then to trigger any `"all"` callbacks.
while (event = events.shift()) {
if (node = calls[event]) {
tail = node.tail;
while ((node = node.next) !== tail) {
node.callback.apply(node.context || this, rest);
}
}
if (node = all) {
tail = node.tail;
args = [event].concat(rest);
while ((node = node.next) !== tail) {
node.callback.apply(node.context || this, args);
}
}
}
return this;
}
};
// Aliases for backwards compatibility.
Events.bind = Events.on;
Events.unbind = Events.off;
// Backbone.Model
// --------------
// Create a new model, with defined attributes. A client id (`cid`)
// is automatically generated and assigned for you.
var Model = Backbone.Model = function(attributes, options) {
var defaults;
attributes || (attributes = {});
if (options && options.parse) attributes = this.parse(attributes);
if (defaults = getValue(this, 'defaults')) {
attributes = _.extend({}, defaults, attributes);
}
if (options && options.collection) this.collection = options.collection;
this.attributes = {};
this._escapedAttributes = {};
this.cid = _.uniqueId('c');
this.changed = {};
this._silent = {};
this._pending = {};
this.set(attributes, {silent: true});
// Reset change tracking.
this.changed = {};
this._silent = {};
this._pending = {};
this._previousAttributes = _.clone(this.attributes);
this.initialize.apply(this, arguments);
};
// Attach all inheritable methods to the Model prototype.
_.extend(Model.prototype, Events, {
// A hash of attributes whose current and previous value differ.
changed: null,
// A hash of attributes that have silently changed since the last time
// `change` was called. Will become pending attributes on the next call.
_silent: null,
// A hash of attributes that have changed since the last `'change'` event
// began.
_pending: null,
// The default name for the JSON `id` attribute is `"id"`. MongoDB and
// CouchDB users may want to set this to `"_id"`.
idAttribute: 'id',
// Initialize is an empty function by default. Override it with your own
// initialization logic.
initialize: function(){},
// Return a copy of the model's `attributes` object.
toJSON: function(options) {
return _.clone(this.attributes);
},
// Get the value of an attribute.
get: function(attr) {
return this.attributes[attr];
},
// Get the HTML-escaped value of an attribute.
escape: function(attr) {
var html;
if (html = this._escapedAttributes[attr]) return html;
var val = this.get(attr);
return this._escapedAttributes[attr] = _.escape(val == null ? '' : '' + val);
},
// Returns `true` if the attribute contains a value that is not null
// or undefined.
has: function(attr) {
return this.get(attr) != null;
},
// Set a hash of model attributes on the object, firing `"change"` unless
// you choose to silence it.
set: function(key, value, options) {
var attrs, attr, val;
// Handle both `"key", value` and `{key: value}` -style arguments.
if (_.isObject(key) || key == null) {
attrs = key;
options = value;
} else {
attrs = {};
attrs[key] = value;
}
// Extract attributes and options.
options || (options = {});
if (!attrs) return this;
if (attrs instanceof Model) attrs = attrs.attributes;
if (options.unset) for (attr in attrs) attrs[attr] = void 0;
// Run validation.
if (!this._validate(attrs, options)) return false;
// Check for changes of `id`.
if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
var changes = options.changes = {};
var now = this.attributes;
var escaped = this._escapedAttributes;
var prev = this._previousAttributes || {};
// For each `set` attribute...
for (attr in attrs) {
val = attrs[attr];
// If the new and current value differ, record the change.
if (!_.isEqual(now[attr], val) || (options.unset && _.has(now, attr))) {
delete escaped[attr];
(options.silent ? this._silent : changes)[attr] = true;
}
// Update or delete the current value.
options.unset ? delete now[attr] : now[attr] = val;
// If the new and previous value differ, record the change. If not,
// then remove changes for this attribute.
if (!_.isEqual(prev[attr], val) || (_.has(now, attr) != _.has(prev, attr))) {
this.changed[attr] = val;
if (!options.silent) this._pending[attr] = true;
} else {
delete this.changed[attr];
delete this._pending[attr];
}
}
// Fire the `"change"` events.
if (!options.silent) this.change(options);
return this;
},
// Remove an attribute from the model, firing `"change"` unless you choose
// to silence it. `unset` is a noop if the attribute doesn't exist.
unset: function(attr, options) {
(options || (options = {})).unset = true;
return this.set(attr, null, options);
},
// Clear all attributes on the model, firing `"change"` unless you choose
// to silence it.
clear: function(options) {
(options || (options = {})).unset = true;
return this.set(_.clone(this.attributes), options);
},
// Fetch the model from the server. If the server's representation of the
// model differs from its current attributes, they will be overriden,
// triggering a `"change"` event.
fetch: function(options) {
options = options ? _.clone(options) : {};
var model = this;
var success = options.success;
options.success = function(resp, status, xhr) {
if (!model.set(model.parse(resp, xhr), options)) return false;
if (success) success(model, resp);
};
options.error = Backbone.wrapError(options.error, model, options);
return (this.sync || Backbone.sync).call(this, 'read', this, options);
},
// Set a hash of model attributes, and sync the model to the server.
// If the server returns an attributes hash that differs, the model's
// state will be `set` again.
save: function(key, value, options) {
var attrs, current;
// Handle both `("key", value)` and `({key: value})` -style calls.
if (_.isObject(key) || key == null) {
attrs = key;
options = value;
} else {
attrs = {};
attrs[key] = value;
}
options = options ? _.clone(options) : {};
// If we're "wait"-ing to set changed attributes, validate early.
if (options.wait) {
if (!this._validate(attrs, options)) return false;
current = _.clone(this.attributes);
}
// Regular saves `set` attributes before persisting to the server.
var silentOptions = _.extend({}, options, {silent: true});
if (attrs && !this.set(attrs, options.wait ? silentOptions : options)) {
return false;
}
// After a successful server-side save, the client is (optionally)
// updated with the server-side state.
var model = this;
var success = options.success;
options.success = function(resp, status, xhr) {
var serverAttrs = model.parse(resp, xhr);
if (options.wait) {
delete options.wait;
serverAttrs = _.extend(attrs || {}, serverAttrs);
}
if (!model.set(serverAttrs, options)) return false;
if (success) {
success(model, resp);
} else {
model.trigger('sync', model, resp, options);
}
};
// Finish configuring and sending the Ajax request.
options.error = Backbone.wrapError(options.error, model, options);
var method = this.isNew() ? 'create' : 'update';
var xhr = (this.sync || Backbone.sync).call(this, method, this, options);
if (options.wait) this.set(current, silentOptions);
return xhr;
},
// Destroy this model on the server if it was already persisted.
// Optimistically removes the model from its collection, if it has one.
// If `wait: true` is passed, waits for the server to respond before removal.
destroy: function(options) {
options = options ? _.clone(options) : {};
var model = this;
var success = options.success;
var triggerDestroy = function() {
model.trigger('destroy', model, model.collection, options);
};
if (this.isNew()) {
triggerDestroy();
return false;
}
options.success = function(resp) {
if (options.wait) triggerDestroy();
if (success) {
success(model, resp);
} else {
model.trigger('sync', model, resp, options);
}
};
options.error = Backbone.wrapError(options.error, model, options);
var xhr = (this.sync || Backbone.sync).call(this, 'delete', this, options);
if (!options.wait) triggerDestroy();
return xhr;
},
// Default URL for the model's representation on the server -- if you're
// using Backbone's restful methods, override this to change the endpoint
// that will be called.
url: function() {
var base = getValue(this, 'urlRoot') || getValue(this.collection, 'url') || urlError();
if (this.isNew()) return base;
return base + (base.charAt(base.length - 1) == '/' ? '' : '/') + encodeURIComponent(this.id);
},
// **parse** converts a response into the hash of attributes to be `set` on
// the model. The default implementation is just to pass the response along.
parse: function(resp, xhr) {
return resp;
},
// Create a new model with identical attributes to this one.
clone: function() {
return new this.constructor(this.attributes);
},
// A model is new if it has never been saved to the server, and lacks an id.
isNew: function() {
return this.id == null;
},
// Call this method to manually fire a `"change"` event for this model and
// a `"change:attribute"` event for each changed attribute.
// Calling this will cause all objects observing the model to update.
change: function(options) {
options || (options = {});
var changing = this._changing;
this._changing = true;
// Silent changes become pending changes.
for (var attr in this._silent) this._pending[attr] = true;
// Silent changes are triggered.
var changes = _.extend({}, options.changes, this._silent);
this._silent = {};
for (var attr in changes) {
this.trigger('change:' + attr, this, this.get(attr), options);
}
if (changing) return this;
// Continue firing `"change"` events while there are pending changes.
while (!_.isEmpty(this._pending)) {
this._pending = {};
this.trigger('change', this, options);
// Pending and silent changes still remain.
for (var attr in this.changed) {
if (this._pending[attr] || this._silent[attr]) continue;
delete this.changed[attr];
}
this._previousAttributes = _.clone(this.attributes);
}
this._changing = false;
return this;
},
// Determine if the model has changed since the last `"change"` event.
// If you specify an attribute name, determine if that attribute has changed.
hasChanged: function(attr) {
if (!arguments.length) return !_.isEmpty(this.changed);
return _.has(this.changed, attr);
},
// Return an object containing all the attributes that have changed, or
// false if there are no changed attributes. Useful for determining what
// parts of a view need to be updated and/or what attributes need to be
// persisted to the server. Unset attributes will be set to undefined.
// You can also pass an attributes object to diff against the model,
// determining if there *would be* a change.
changedAttributes: function(diff) {
if (!diff) return this.hasChanged() ? _.clone(this.changed) : false;
var val, changed = false, old = this._previousAttributes;
for (var attr in diff) {
if (_.isEqual(old[attr], (val = diff[attr]))) continue;
(changed || (changed = {}))[attr] = val;
}
return changed;
},
// Get the previous value of an attribute, recorded at the time the last
// `"change"` event was fired.
previous: function(attr) {
if (!arguments.length || !this._previousAttributes) return null;
return this._previousAttributes[attr];
},
// Get all of the attributes of the model at the time of the previous
// `"change"` event.
previousAttributes: function() {
return _.clone(this._previousAttributes);
},
// Check if the model is currently in a valid state. It's only possible to
// get into an *invalid* state if you're using silent changes.
isValid: function() {
return !this.validate(this.attributes);
},
// Run validation against the next complete set of model attributes,
// returning `true` if all is well. If a specific `error` callback has
// been passed, call that instead of firing the general `"error"` event.
_validate: function(attrs, options) {
if (options.silent || !this.validate) return true;
attrs = _.extend({}, this.attributes, attrs);
var error = this.validate(attrs, options);
if (!error) return true;
if (options && options.error) {
options.error(this, error, options);
} else {
this.trigger('error', this, error, options);
}
return false;
}
});
// Backbone.Collection
// -------------------
// Provides a standard collection class for our sets of models, ordered
// or unordered. If a `comparator` is specified, the Collection will maintain
// its models in sort order, as they're added and removed.
var Collection = Backbone.Collection = function(models, options) {
options || (options = {});
if (options.model) this.model = options.model;
if (options.comparator) this.comparator = options.comparator;
this._reset();
this.initialize.apply(this, arguments);
if (models) this.reset(models, {silent: true, parse: options.parse});
};
// Define the Collection's inheritable methods.
_.extend(Collection.prototype, Events, {
// The default model for a collection is just a **Backbone.Model**.
// This should be overridden in most cases.
model: Model,
// Initialize is an empty function by default. Override it with your own
// initialization logic.
initialize: function(){},
// The JSON representation of a Collection is an array of the
// models' attributes.
toJSON: function(options) {
return this.map(function(model){ return model.toJSON(options); });
},
// Add a model, or list of models to the set. Pass **silent** to avoid
// firing the `add` event for every new model.
add: function(models, options) {
var i, index, length, model, cid, id, cids = {}, ids = {}, dups = [];
options || (options = {});
models = _.isArray(models) ? models.slice() : [models];
// Begin by turning bare objects into model references, and preventing
// invalid models or duplicate models from being added.
for (i = 0, length = models.length; i < length; i++) {
if (!(model = models[i] = this._prepareModel(models[i], options))) {
throw new Error("Can't add an invalid model to a collection");
}
cid = model.cid;
id = model.id;
if (cids[cid] || this._byCid[cid] || ((id != null) && (ids[id] || this._byId[id]))) {
dups.push(i);
continue;
}
cids[cid] = ids[id] = model;
}
// Remove duplicates.
i = dups.length;
while (i--) {
models.splice(dups[i], 1);
}
// Listen to added models' events, and index models for lookup by
// `id` and by `cid`.
for (i = 0, length = models.length; i < length; i++) {
(model = models[i]).on('all', this._onModelEvent, this);
this._byCid[model.cid] = model;
if (model.id != null) this._byId[model.id] = model;
}
// Insert models into the collection, re-sorting if needed, and triggering
// `add` events unless silenced.
this.length += length;
index = options.at != null ? options.at : this.models.length;
splice.apply(this.models, [index, 0].concat(models));
if (this.comparator) this.sort({silent: true});
if (options.silent) return this;
for (i = 0, length = this.models.length; i < length; i++) {
if (!cids[(model = this.models[i]).cid]) continue;
options.index = i;
model.trigger('add', model, this, options);
}
return this;
},
// Remove a model, or a list of models from the set. Pass silent to avoid
// firing the `remove` event for every model removed.
remove: function(models, options) {
var i, l, index, model;
options || (options = {});
models = _.isArray(models) ? models.slice() : [models];
for (i = 0, l = models.length; i < l; i++) {
model = this.getByCid(models[i]) || this.get(models[i]);
if (!model) continue;
delete this._byId[model.id];
delete this._byCid[model.cid];
index = this.indexOf(model);
this.models.splice(index, 1);
this.length--;
if (!options.silent) {
options.index = index;
model.trigger('remove', model, this, options);
}
this._removeReference(model);
}
return this;
},
// Add a model to the end of the collection.
push: function(model, options) {
model = this._prepareModel(model, options);
this.add(model, options);
return model;
},
// Remove a model from the end of the collection.
pop: function(options) {
var model = this.at(this.length - 1);
this.remove(model, options);
return model;
},
// Add a model to the beginning of the collection.
unshift: function(model, options) {
model = this._prepareModel(model, options);
this.add(model, _.extend({at: 0}, options));
return model;
},
// Remove a model from the beginning of the collection.
shift: function(options) {
var model = this.at(0);
this.remove(model, options);
return model;
},
// Get a model from the set by id.
get: function(id) {
if (id == null) return void 0;
return this._byId[id.id != null ? id.id : id];
},
// Get a model from the set by client id.
getByCid: function(cid) {
return cid && this._byCid[cid.cid || cid];
},
// Get the model at the given index.
at: function(index) {
return this.models[index];
},
// Return models with matching attributes. Useful for simple cases of `filter`.
where: function(attrs) {
if (_.isEmpty(attrs)) return [];
return this.filter(function(model) {
for (var key in attrs) {
if (attrs[key] !== model.get(key)) return false;
}
return true;
});
},
// Force the collection to re-sort itself. You don't need to call this under
// normal circumstances, as the set will maintain sort order as each item
// is added.
sort: function(options) {
options || (options = {});
if (!this.comparator) throw new Error('Cannot sort a set without a comparator');
var boundComparator = _.bind(this.comparator, this);
if (this.comparator.length == 1) {
this.models = this.sortBy(boundComparator);
} else {
this.models.sort(boundComparator);
}
if (!options.silent) this.trigger('reset', this, options);
return this;
},
// Pluck an attribute from each model in the collection.
pluck: function(attr) {
return _.map(this.models, function(model){ return model.get(attr); });
},
// When you have more items than you want to add or remove individually,
// you can reset the entire set with a new list of models, without firing
// any `add` or `remove` events. Fires `reset` when finished.
reset: function(models, options) {
models || (models = []);
options || (options = {});
for (var i = 0, l = this.models.length; i < l; i++) {
this._removeReference(this.models[i]);
}
this._reset();
this.add(models, _.extend({silent: true}, options));
if (!options.silent) this.trigger('reset', this, options);
return this;
},
// Fetch the default set of models for this collection, resetting the
// collection when they arrive. If `add: true` is passed, appends the
// models to the collection instead of resetting.
fetch: function(options) {
options = options ? _.clone(options) : {};
if (options.parse === undefined) options.parse = true;
var collection = this;
var success = options.success;
options.success = function(resp, status, xhr) {
collection[options.add ? 'add' : 'reset'](collection.parse(resp, xhr), options);
if (success) success(collection, resp);
};
options.error = Backbone.wrapError(options.error, collection, options);
return (this.sync || Backbone.sync).call(this, 'read', this, options);
},
// Create a new instance of a model in this collection. Add the model to the
// collection immediately, unless `wait: true` is passed, in which case we
// wait for the server to agree.
create: function(model, options) {
var coll = this;
options = options ? _.clone(options) : {};
model = this._prepareModel(model, options);
if (!model) return false;
if (!options.wait) coll.add(model, options);
var success = options.success;
options.success = function(nextModel, resp, xhr) {
if (options.wait) coll.add(nextModel, options);
if (success) {
success(nextModel, resp);
} else {
nextModel.trigger('sync', model, resp, options);
}
};
model.save(null, options);
return model;
},
// **parse** converts a response into a list of models to be added to the
// collection. The default implementation is just to pass it through.
parse: function(resp, xhr) {
return resp;
},
// Proxy to _'s chain. Can't be proxied the same way the rest of the
// underscore methods are proxied because it relies on the underscore
// constructor.
chain: function () {
return _(this.models).chain();
},
// Reset all internal state. Called when the collection is reset.
_reset: function(options) {
this.length = 0;
this.models = [];
this._byId = {};
this._byCid = {};
},
// Prepare a model or hash of attributes to be added to this collection.
_prepareModel: function(model, options) {
options || (options = {});
if (!(model instanceof Model)) {
var attrs = model;
options.collection = this;
model = new this.model(attrs, options);
if (!model._validate(model.attributes, options)) model = false;
} else if (!model.collection) {
model.collection = this;
}
return model;
},
// Internal method to remove a model's ties to a collection.
_removeReference: function(model) {
if (this == model.collection) {
delete model.collection;
}
model.off('all', this._onModelEvent, this);
},
// Internal method called every time a model in the set fires an event.
// Sets need to update their indexes when models change ids. All other
// events simply proxy through. "add" and "remove" events that originate
// in other collections are ignored.
_onModelEvent: function(event, model, collection, options) {
if ((event == 'add' || event == 'remove') && collection != this) return;
if (event == 'destroy') {
this.remove(model, options);
}
if (model && event === 'change:' + model.idAttribute) {
delete this._byId[model.previous(model.idAttribute)];
this._byId[model.id] = model;
}
this.trigger.apply(this, arguments);
}
});
// Underscore methods that we want to implement on the Collection.
var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find',
'detect', 'filter', 'select', 'reject', 'every', 'all', 'some', 'any',
'include', 'contains', 'invoke', 'max', 'min', 'sortBy', 'sortedIndex',
'toArray', 'size', 'first', 'initial', 'rest', 'last', 'without', 'indexOf',
'shuffle', 'lastIndexOf', 'isEmpty', 'groupBy'];
// Mix in each Underscore method as a proxy to `Collection#models`.
_.each(methods, function(method) {
Collection.prototype[method] = function() {
return _[method].apply(_, [this.models].concat(_.toArray(arguments)));
};
});
// Backbone.Router
// -------------------
// Routers map faux-URLs to actions, and fire events when routes are
// matched. Creating a new one sets its `routes` hash, if not set statically.
var Router = Backbone.Router = function(options) {
options || (options = {});
if (options.routes) this.routes = options.routes;
this._bindRoutes();
this.initialize.apply(this, arguments);
};
// Cached regular expressions for matching named param parts and splatted
// parts of route strings.
var namedParam = /:\w+/g;
var splatParam = /\*\w+/g;
var escapeRegExp = /[-[\]{}()+?.,\\^$|#\s]/g;
// Set up all inheritable **Backbone.Router** properties and methods.
_.extend(Router.prototype, Events, {
// Initialize is an empty function by default. Override it with your own
// initialization logic.
initialize: function(){},
// Manually bind a single named route to a callback. For example:
//
// this.route('search/:query/p:num', 'search', function(query, num) {
// ...
// });
//
route: function(route, name, callback) {
Backbone.history || (Backbone.history = new History);
if (!_.isRegExp(route)) route = this._routeToRegExp(route);
if (!callback) callback = this[name];
Backbone.history.route(route, _.bind(function(fragment) {
var args = this._extractParameters(route, fragment);
callback && callback.apply(this, args);
this.trigger.apply(this, ['route:' + name].concat(args));
Backbone.history.trigger('route', this, name, args);
}, this));
return this;
},
// Simple proxy to `Backbone.history` to save a fragment into the history.
navigate: function(fragment, options) {
Backbone.history.navigate(fragment, options);
},
// Bind all defined routes to `Backbone.history`. We have to reverse the
// order of the routes here to support behavior where the most general
// routes can be defined at the bottom of the route map.
_bindRoutes: function() {
if (!this.routes) return;
var routes = [];
for (var route in this.routes) {
routes.unshift([route, this.routes[route]]);
}
for (var i = 0, l = routes.length; i < l; i++) {
this.route(routes[i][0], routes[i][1], this[routes[i][1]]);
}
},
// Convert a route string into a regular expression, suitable for matching
// against the current location hash.
_routeToRegExp: function(route) {
route = route.replace(escapeRegExp, '\\$&')
.replace(namedParam, '([^\/]+)')
.replace(splatParam, '(.*?)');
return new RegExp('^' + route + '$');
},
// Given a route, and a URL fragment that it matches, return the array of
// extracted parameters.
_extractParameters: function(route, fragment) {
return route.exec(fragment).slice(1);
}
});
// Backbone.History
// ----------------
// Handles cross-browser history management, based on URL fragments. If the
// browser does not support `onhashchange`, falls back to polling.
var History = Backbone.History = function() {
this.handlers = [];
_.bindAll(this, 'checkUrl');
};
// Cached regex for cleaning leading hashes and slashes .
var routeStripper = /^[#\/]/;
// Cached regex for detecting MSIE.
var isExplorer = /msie [\w.]+/;
// Has the history handling already been started?
History.started = false;
// Set up all inheritable **Backbone.History** properties and methods.
_.extend(History.prototype, Events, {
// The default interval to poll for hash changes, if necessary, is
// twenty times a second.
interval: 50,
// Gets the true hash value. Cannot use location.hash directly due to bug
// in Firefox where location.hash will always be decoded.
getHash: function(windowOverride) {
var loc = windowOverride ? windowOverride.location : window.location;
var match = loc.href.match(/#(.*)$/);
return match ? match[1] : '';
},
// Get the cross-browser normalized URL fragment, either from the URL,
// the hash, or the override.
getFragment: function(fragment, forcePushState) {
if (fragment == null) {
if (this._hasPushState || forcePushState) {
fragment = window.location.pathname;
var search = window.location.search;
if (search) fragment += search;
} else {
fragment = this.getHash();
}
}
if (!fragment.indexOf(this.options.root)) fragment = fragment.substr(this.options.root.length);
return fragment.replace(routeStripper, '');
},
// Start the hash change handling, returning `true` if the current URL matches
// an existing route, and `false` otherwise.
start: function(options) {
if (History.started) throw new Error("Backbone.history has already been started");
History.started = true;
// Figure out the initial configuration. Do we need an iframe?
// Is pushState desired ... is it available?
this.options = _.extend({}, {root: '/'}, this.options, options);
this._wantsHashChange = this.options.hashChange !== false;
this._wantsPushState = !!this.options.pushState;
this._hasPushState = !!(this.options.pushState && window.history && window.history.pushState);
var fragment = this.getFragment();
var docMode = document.documentMode;
var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7));
if (oldIE) {
this.iframe = $('').hide().appendTo('body')[0].contentWindow;
this.navigate(fragment);
}
// Depending on whether we're using pushState or hashes, and whether
// 'onhashchange' is supported, determine how we check the URL state.
if (this._hasPushState) {
$(window).bind('popstate', this.checkUrl);
} else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) {
$(window).bind('hashchange', this.checkUrl);
} else if (this._wantsHashChange) {
this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
}
// Determine if we need to change the base url, for a pushState link
// opened by a non-pushState browser.
this.fragment = fragment;
var loc = window.location;
var atRoot = loc.pathname == this.options.root;
// If we've started off with a route from a `pushState`-enabled browser,
// but we're currently in a browser that doesn't support it...
if (this._wantsHashChange && this._wantsPushState && !this._hasPushState && !atRoot) {
this.fragment = this.getFragment(null, true);
window.location.replace(this.options.root + '#' + this.fragment);
// Return immediately as browser will do redirect to new url
return true;
// Or if we've started out with a hash-based route, but we're currently
// in a browser where it could be `pushState`-based instead...
} else if (this._wantsPushState && this._hasPushState && atRoot && loc.hash) {
this.fragment = this.getHash().replace(routeStripper, '');
window.history.replaceState({}, document.title, loc.protocol + '//' + loc.host + this.options.root + this.fragment);
}
if (!this.options.silent) {
return this.loadUrl();
}
},
// Disable Backbone.history, perhaps temporarily. Not useful in a real app,
// but possibly useful for unit testing Routers.
stop: function() {
$(window).unbind('popstate', this.checkUrl).unbind('hashchange', this.checkUrl);
clearInterval(this._checkUrlInterval);
History.started = false;
},
// Add a route to be tested when the fragment changes. Routes added later
// may override previous routes.
route: function(route, callback) {
this.handlers.unshift({route: route, callback: callback});
},
// Checks the current URL to see if it has changed, and if it has,
// calls `loadUrl`, normalizing across the hidden iframe.
checkUrl: function(e) {
var current = this.getFragment();
if (current == this.fragment && this.iframe) current = this.getFragment(this.getHash(this.iframe));
if (current == this.fragment) return false;
if (this.iframe) this.navigate(current);
this.loadUrl() || this.loadUrl(this.getHash());
},
// Attempt to load the current URL fragment. If a route succeeds with a
// match, returns `true`. If no defined routes matches the fragment,
// returns `false`.
loadUrl: function(fragmentOverride) {
var fragment = this.fragment = this.getFragment(fragmentOverride);
var matched = _.any(this.handlers, function(handler) {
if (handler.route.test(fragment)) {
handler.callback(fragment);
return true;
}
});
return matched;
},
// Save a fragment into the hash history, or replace the URL state if the
// 'replace' option is passed. You are responsible for properly URL-encoding
// the fragment in advance.
//
// The options object can contain `trigger: true` if you wish to have the
// route callback be fired (not usually desirable), or `replace: true`, if
// you wish to modify the current URL without adding an entry to the history.
navigate: function(fragment, options) {
if (!History.started) return false;
if (!options || options === true) options = {trigger: options};
var frag = (fragment || '').replace(routeStripper, '');
if (this.fragment == frag) return;
// If pushState is available, we use it to set the fragment as a real URL.
if (this._hasPushState) {
if (frag.indexOf(this.options.root) != 0) frag = this.options.root + frag;
this.fragment = frag;
window.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, frag);
// If hash changes haven't been explicitly disabled, update the hash
// fragment to store history.
} else if (this._wantsHashChange) {
this.fragment = frag;
this._updateHash(window.location, frag, options.replace);
if (this.iframe && (frag != this.getFragment(this.getHash(this.iframe)))) {
// Opening and closing the iframe tricks IE7 and earlier to push a history entry on hash-tag change.
// When replace is true, we don't want this.
if(!options.replace) this.iframe.document.open().close();
this._updateHash(this.iframe.location, frag, options.replace);
}
// If you've told us that you explicitly don't want fallback hashchange-
// based history, then `navigate` becomes a page refresh.
} else {
window.location.assign(this.options.root + fragment);
}
if (options.trigger) this.loadUrl(fragment);
},
// Update the hash location, either replacing the current entry, or adding
// a new one to the browser history.
_updateHash: function(location, fragment, replace) {
if (replace) {
location.replace(location.toString().replace(/(javascript:|#).*$/, '') + '#' + fragment);
} else {
location.hash = fragment;
}
}
});
// Backbone.View
// -------------
// Creating a Backbone.View creates its initial element outside of the DOM,
// if an existing element is not provided...
var View = Backbone.View = function(options) {
this.cid = _.uniqueId('view');
this._configure(options || {});
this._ensureElement();
this.initialize.apply(this, arguments);
this.delegateEvents();
};
// Cached regex to split keys for `delegate`.
var delegateEventSplitter = /^(\S+)\s*(.*)$/;
// List of view options to be merged as properties.
var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName'];
// Set up all inheritable **Backbone.View** properties and methods.
_.extend(View.prototype, Events, {
// The default `tagName` of a View's element is `"div"`.
tagName: 'div',
// jQuery delegate for element lookup, scoped to DOM elements within the
// current view. This should be prefered to global lookups where possible.
$: function(selector) {
return this.$el.find(selector);
},
// Initialize is an empty function by default. Override it with your own
// initialization logic.
initialize: function(){},
// **render** is the core function that your view should override, in order
// to populate its element (`this.el`), with the appropriate HTML. The
// convention is for **render** to always return `this`.
render: function() {
return this;
},
// Remove this view from the DOM. Note that the view isn't present in the
// DOM by default, so calling this method may be a no-op.
remove: function() {
this.$el.remove();
return this;
},
// For small amounts of DOM Elements, where a full-blown template isn't
// needed, use **make** to manufacture elements, one at a time.
//
// var el = this.make('li', {'class': 'row'}, this.model.escape('title'));
//
make: function(tagName, attributes, content) {
var el = document.createElement(tagName);
if (attributes) $(el).attr(attributes);
if (content) $(el).html(content);
return el;
},
// Change the view's element (`this.el` property), including event
// re-delegation.
setElement: function(element, delegate) {
if (this.$el) this.undelegateEvents();
this.$el = (element instanceof $) ? element : $(element);
this.el = this.$el[0];
if (delegate !== false) this.delegateEvents();
return this;
},
// Set callbacks, where `this.events` is a hash of
//
// *{"event selector": "callback"}*
//
// {
// 'mousedown .title': 'edit',
// 'click .button': 'save'
// 'click .open': function(e) { ... }
// }
//
// pairs. Callbacks will be bound to the view, with `this` set properly.
// Uses event delegation for efficiency.
// Omitting the selector binds the event to `this.el`.
// This only works for delegate-able events: not `focus`, `blur`, and
// not `change`, `submit`, and `reset` in Internet Explorer.
delegateEvents: function(events) {
if (!(events || (events = getValue(this, 'events')))) return;
this.undelegateEvents();
for (var key in events) {
var method = events[key];
if (!_.isFunction(method)) method = this[events[key]];
if (!method) throw new Error('Method "' + events[key] + '" does not exist');
var match = key.match(delegateEventSplitter);
var eventName = match[1], selector = match[2];
method = _.bind(method, this);
eventName += '.delegateEvents' + this.cid;
if (selector === '') {
this.$el.bind(eventName, method);
} else {
this.$el.delegate(selector, eventName, method);
}
}
},
// Clears all callbacks previously bound to the view with `delegateEvents`.
// You usually don't need to use this, but may wish to if you have multiple
// Backbone views attached to the same DOM element.
undelegateEvents: function() {
this.$el.unbind('.delegateEvents' + this.cid);
},
// Performs the initial configuration of a View with a set of options.
// Keys with special meaning *(model, collection, id, className)*, are
// attached directly to the view.
_configure: function(options) {
if (this.options) options = _.extend({}, this.options, options);
for (var i = 0, l = viewOptions.length; i < l; i++) {
var attr = viewOptions[i];
if (options[attr]) this[attr] = options[attr];
}
this.options = options;
},
// Ensure that the View has a DOM element to render into.
// If `this.el` is a string, pass it through `$()`, take the first
// matching element, and re-assign it to `el`. Otherwise, create
// an element from the `id`, `className` and `tagName` properties.
_ensureElement: function() {
if (!this.el) {
var attrs = getValue(this, 'attributes') || {};
if (this.id) attrs.id = this.id;
if (this.className) attrs['class'] = this.className;
this.setElement(this.make(this.tagName, attrs), false);
} else {
this.setElement(this.el, false);
}
}
});
// The self-propagating extend function that Backbone classes use.
var extend = function (protoProps, classProps) {
var child = inherits(this, protoProps, classProps);
child.extend = this.extend;
return child;
};
// Set up inheritance for the model, collection, and view.
Model.extend = Collection.extend = Router.extend = View.extend = extend;
// Backbone.sync
// -------------
// Map from CRUD to HTTP for our default `Backbone.sync` implementation.
var methodMap = {
'create': 'POST',
'update': 'PUT',
'delete': 'DELETE',
'read': 'GET'
};
// Override this function to change the manner in which Backbone persists
// models to the server. You will be passed the type of request, and the
// model in question. By default, makes a RESTful Ajax request
// to the model's `url()`. Some possible customizations could be:
//
// * Use `setTimeout` to batch rapid-fire updates into a single request.
// * Send up the models as XML instead of JSON.
// * Persist models via WebSockets instead of Ajax.
//
// Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests
// as `POST`, with a `_method` parameter containing the true HTTP method,
// as well as all requests with the body as `application/x-www-form-urlencoded`
// instead of `application/json` with the model in a param named `model`.
// Useful when interfacing with server-side languages like **PHP** that make
// it difficult to read the body of `PUT` requests.
Backbone.sync = function(method, model, options) {
var type = methodMap[method];
// Default options, unless specified.
options || (options = {});
// Default JSON-request options.
var params = {type: type, dataType: 'json'};
// Ensure that we have a URL.
if (!options.url) {
params.url = getValue(model, 'url') || urlError();
}
// Ensure that we have the appropriate request data.
if (!options.data && model && (method == 'create' || method == 'update')) {
params.contentType = 'application/json';
params.data = JSON.stringify(model.toJSON());
}
// For older servers, emulate JSON by encoding the request into an HTML-form.
if (Backbone.emulateJSON) {
params.contentType = 'application/x-www-form-urlencoded';
params.data = params.data ? {model: params.data} : {};
}
// For older servers, emulate HTTP by mimicking the HTTP method with `_method`
// And an `X-HTTP-Method-Override` header.
if (Backbone.emulateHTTP) {
if (type === 'PUT' || type === 'DELETE') {
if (Backbone.emulateJSON) params.data._method = type;
params.type = 'POST';
params.beforeSend = function(xhr) {
xhr.setRequestHeader('X-HTTP-Method-Override', type);
};
}
}
// Don't process data on a non-GET request.
if (params.type !== 'GET' && !Backbone.emulateJSON) {
params.processData = false;
}
// Make the request, allowing the user to override any Ajax options.
return $.ajax(_.extend(params, options));
};
// Wrap an optional error callback with a fallback error event.
Backbone.wrapError = function(onError, originalModel, options) {
return function(model, resp) {
resp = model === originalModel ? resp : model;
if (onError) {
onError(originalModel, resp, options);
} else {
originalModel.trigger('error', originalModel, resp, options);
}
};
};
// Helpers
// -------
// Shared empty constructor function to aid in prototype-chain creation.
var ctor = function(){};
// Helper function to correctly set up the prototype chain, for subclasses.
// Similar to `goog.inherits`, but uses a hash of prototype properties and
// class properties to be extended.
var inherits = function(parent, protoProps, staticProps) {
var child;
// The constructor function for the new subclass is either defined by you
// (the "constructor" property in your `extend` definition), or defaulted
// by us to simply call the parent's constructor.
if (protoProps && protoProps.hasOwnProperty('constructor')) {
child = protoProps.constructor;
} else {
child = function(){ parent.apply(this, arguments); };
}
// Inherit class (static) properties from parent.
_.extend(child, parent);
// Set the prototype chain to inherit from `parent`, without calling
// `parent`'s constructor function.
ctor.prototype = parent.prototype;
child.prototype = new ctor();
// Add prototype properties (instance properties) to the subclass,
// if supplied.
if (protoProps) _.extend(child.prototype, protoProps);
// Add static properties to the constructor function, if supplied.
if (staticProps) _.extend(child, staticProps);
// Correctly set child's `prototype.constructor`.
child.prototype.constructor = child;
// Set a convenience property in case the parent's prototype is needed later.
child.__super__ = parent.prototype;
return child;
};
// Helper function to get a value from a Backbone object as a property
// or as a function.
var getValue = function(object, prop) {
if (!(object && object[prop])) return null;
return _.isFunction(object[prop]) ? object[prop]() : object[prop];
};
// Throw an error when a URL is needed, and none is supplied.
var urlError = function() {
throw new Error('A "url" property or function must be specified');
};
}).call(this);
/** Utility script to reset things to a more clean state after usage */
vipjQuery = jQuery.noConflict(true);
vipUnderscore = _.noConflict();
vipBackbone = Backbone.noConflict();
// underi18n v0.1
// (c) 2012 Yiorgis Gozadinos, Crypho AS, http://crypho.com
// underi18n is distributed under the MIT license.
// http://github.com/ggozad/underi18n
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define([], function () {
return (root.underi18n = factory());
});
} else {
// Browser globals
root.underi18n = factory();
}
}(this, function () {
var underi18n = {
MessageFactory: (function () {
var MessageFactory = function (catalog) {
this.translate = function (msgid, keywords) {
var msgstr = catalog[msgid] ? catalog[msgid] : msgid;
if (keywords) {
for (var key in keywords) {
msgstr = msgstr.replace('${'+key+'}', keywords[key]);
}
}
return msgstr;
};
};
return function (catalog) {
return new MessageFactory(catalog).translate;
};
})(),
template: function (str, factory) {
var s = this.templateSettings;
return str.replace(s.translate, function (match, code) {
return factory(
code
.replace(/^\s+|\s+$/g, '')
);
}).replace(/\$\{(.*?)\}/g, function (m, c) {
return s.i18nVarLeftDel + c + s.i18nVarRightDel;
});
},
templateSettings: {
translate: /<@_([\s\S]+?)@>/g,
i18nVarLeftDel: '<@=',
i18nVarRightDel: '@>'
}
};
return underi18n;
}));
if (!PlatformBrowserSupport) { // make sure this file runs only once;
var PlatformBrowserSupport = (function() {
var api = {};
// RC Supported - (Vista and IE8)
api.isTrustedDeviceSupported = function() {
//(RC - Vista - IE8) U (Firefox on Mac)
return (api.isRCSupported() && !api.isWindowsVista() && !api.isIE8OrBelow()) || (api.isMac() && api.isFirefox());
};
// suportCORS should return true for FF 3.6+, Safari 5+, IE 7+
api.isRCSupported = function() {
try {
//Provisioning and validation on Windows XP is not supported.
if(api.isWinXP()){
return false;
}
if (BrowserDetect.OS.indexOf("Windows") != -1) {
if (BrowserDetect.browser.indexOf("Explorer") != -1) {
if (BrowserDetect.version >= 7.0) {
if( (BrowserDetect.version >= 7 && BrowserDetect.version <8) || (BrowserDetect.version >= 10) ){
// If the browser is 10.0 (this could be Metro or Desktop, so check activeX to test support)
// NOTE: This is not completely fool proof, this is only best effort, if someone disables activeX on desktop IE, this will return false for RC support
return BrowserDetect.isActivexEnabled();
}
return true;
}
} else if (BrowserDetect.browser.indexOf("Firefox") != -1) {
if (BrowserDetect.version >= 3.6) {
return true;
}
} else if (BrowserDetect.browser.indexOf("Chrome") != -1) {
if (BrowserDetect.version >= 3) {
return true;
}
/* Un-comment this to re-add Safari support on Windows
} else if (BrowserDetect.browser.indexOf("Safari") != -1) {
if (BrowserDetect.version >= 5.0) {
return true;
}
*/
}
} else if (BrowserDetect.OS.indexOf("Mac") != -1) {
if (BrowserDetect.browser.indexOf("Safari") != -1) {
if (BrowserDetect.version >= 5.0) {
return true;
}
} /* Un-comment this to re-add Firefox support on OSX
else if (BrowserDetect.browser.indexOf("Firefox") != -1) {
if (BrowserDetect.version >= 3.6) {
return true;
}*/
else if (BrowserDetect.browser.indexOf("Chrome") != -1) {
if (BrowserDetect.version >= 3) {
return true;
}
}
}
} catch (err) {
return false;
}
return false;
};
// suportCORS should return true for FF 3.5+, Chrome 3+, Safari 4+, IE 8+
api.isCORSSupported = function() {
try {
if (BrowserDetect.browser.indexOf("Explorer") != -1) {
if (BrowserDetect.version >= 8.0 && BrowserDetect.version < 11) {
// README: Some of the IE 11 disables CORS, it is safer to say IE11 does NOT support CORS
return true;
}
} else if (BrowserDetect.browser.indexOf("Firefox") != -1) {
if (BrowserDetect.version >= 3.5) {
return true;
}
} else if (BrowserDetect.browser.indexOf("Chrome") != -1) {
if (BrowserDetect.version >= 3) {
return true;
}
} else if (BrowserDetect.browser.indexOf("Safari") != -1) {
if (BrowserDetect.version >= 4) {
return true;
}
}
} catch (err) {
return false;
}
return false;
};
api.isXDRSupported = function () {
return BrowserDetect.browser === "Explorer" && window.XDomainRequest;
};
api.isPostMessageUnsupported = function() {
if(BrowserDetect.browser.indexOf("Explorer")!=-1){
if(BrowserDetect.version < 8.0){
return true;
}
}
return false;
};
api.isIE8OrBelow = function() {
if(BrowserDetect.browser.indexOf("Explorer")!=-1){
if(BrowserDetect.version < 9.0){
return true;
}
}
return false;
};
api.isIE11 = function() {
if(BrowserDetect.browser.indexOf("Explorer")!=-1){
if(BrowserDetect.version == 11){
return true;
}
}
return false;
};
api.isWinXP = function() {
if(BrowserDetect.osVersion == "Windows XP"){
return true;
}
return false;
};
api.isWindowsVista = function() {
if(BrowserDetect.osVersion == "Windows Vista"){
return true;
}
return false;
};
api.isMac = function() {
if(BrowserDetect.OS.indexOf("Mac") != -1){
return true;
}
return false;
};
api.isFirefox = function () {
if (BrowserDetect.browser.indexOf("Firefox") != -1) {
return true;
}
return false;
};
// api.isWindows8 = function () {
// try {
// if (BrowserDetect.OS.indexOf("Windows") != -1) {
// if (BrowserDetect.osVersion.indexOf("Windows 8") != -1) {
// return true;
// }
// }
// } catch (err) {
// return false;
// }
// return false;
// };
api.getBrowserNameAndVersion = function(){
var browser = BrowserDetect.browser;
if(browser != "An unknown browser"){
browser = browser + " " + BrowserDetect.version;
}
return browser;
};
api.getOSNameAndArch = function(){
var os = BrowserDetect.osVersion;
return os;
};
return api;
})();
var BrowserDetect = {
init : function() {
this.browser = this.searchString(this.dataBrowser)||"An unknown browser";
this.version = this.searchVersion(navigator.userAgent)||this.searchVersion(navigator.appVersion)||"an unknown version";
this.OS = this.searchString(this.dataOS) || "an unknown OS";
this.osVersion = this.searchString(this.osVersion) || "an unknown OS Version";
this.arch = this.searchString(this.dataArch) || "X86";
var ieDetection = this.detectIE();
if (ieDetection) {
this.browser = ieDetection.name === "MSIE" ? "Explorer" : this.browser;
this.version = ieDetection.version;
}
},
searchString : function(data) {
for ( var i = 0; i < data.length; i++) {
var dataString = data[i].string;
var dataProp = data[i].prop;
this.versionSearchString = data[i].versionSearch
|| data[i].identity;
if (dataString) {
if (dataString.indexOf(data[i].subString) != -1)
return data[i].identity;
} else if (dataProp) {
return data[i].identity;
}
}
},
isActivexEnabled : function() {// This is only for Windows
var supported = null;
try {
supported = !!new ActiveXObject("htmlfile");
} catch (e) {
supported = false;
}
return supported;
},
searchVersion : function(dataString) {
var index = dataString.indexOf(this.versionSearchString);
if (index == -1)
return;
return parseFloat(dataString.substring(index
+ this.versionSearchString.length + 1));
},
dataBrowser : [ {
//Currently we didn't support Edge on Windows 10
string : navigator.userAgent,
subString : "Edge",
identity : "Edge"
},{
string : navigator.userAgent,
subString : "Chrome",
identity : "Chrome"
}, {
string : navigator.userAgent,
subString : "OmniWeb",
versionSearch : "OmniWeb/",
identity : "OmniWeb"
}, {
string : navigator.vendor,
subString : "Apple",
identity : "Safari",
versionSearch : "Version"
}, {
prop : window.opera,
identity : "Opera"
}, {
string : navigator.vendor,
subString : "iCab",
identity : "iCab"
}, {
string : navigator.vendor,
subString : "KDE",
identity : "Konqueror"
}, {
string : navigator.userAgent,
subString : "Firefox",
identity : "Firefox"
}, {
string : navigator.vendor,
subString : "Camino",
identity : "Camino"
}, { // for newer Netscapes (6+)
string : navigator.userAgent,
subString : "Netscape",
identity : "Netscape"
}, {
string : navigator.userAgent,
subString : "MSIE",
identity : "Explorer",
versionSearch : "MSIE"
}, {
string : navigator.userAgent,
subString : "Gecko",
identity : "Mozilla",
versionSearch : "rv"
}, { // for older Netscapes (4-)
string : navigator.userAgent,
subString : "Mozilla",
identity : "Netscape",
versionSearch : "Mozilla"
} ],
dataOS : [ {
string : navigator.platform,
subString : "Win",
identity : "Windows"
}, {
string : navigator.platform,
subString : "Mac",
identity : "Mac"
}, {
string : navigator.userAgent,
subString : "iPhone",
identity : "iPhone/iPod"
}, {
string : navigator.platform,
subString : "Linux",
identity : "Linux"
} ],
osVersion : [ {
string : navigator.userAgent,
subString : "Windows NT 10.0",
identity : "Windows 10"
},{
string : navigator.userAgent,
subString : "Windows NT 6.3",
identity : "Windows 8.1"
}, {
string : navigator.userAgent,
subString : "Windows NT 6.2",
identity : "Windows 8"
}, {
string : navigator.userAgent,
subString : "Windows NT 6.1",
identity : "Windows 7"
}, {
string : navigator.userAgent,
subString : "Windows NT 6.0",
identity : "Windows Vista"
}, {
string : navigator.userAgent,
subString : "Windows NT 5.2",
identity : "Windows Server 2003"
}, {
string : navigator.userAgent,
subString : "Windows NT 5.1",
identity : "Windows XP"
}, {
string : navigator.userAgent,
subString : "Windows XP",
identity : "Windows XP"
}, {
string : navigator.userAgent,
subString : "Mac_PowerPC",
identity : "Mac OS"
}, {
string : navigator.userAgent,
subString : "Macintosh",
identity : "Mac OS"
} ],
dataArch : [ {
string : navigator.cpuClass,
subString : "x64",
identity : "X86_64"
}, {
string : navigator.cpuClass,
subString : "x86",
identity : "X86"
}, {
string : navigator.platform,
subString : "win32",
identity : "X86"
}, {
string : navigator.platform,
subString : "win64",
identity : "X86_64"
}, {
string : navigator.platform,
subString : "Win32",
identity : "X86"
}, {
string : navigator.platform,
subString : "Win64",
identity : "X86_64"
}, {
string : navigator.userAgent,
subString : "WOW64",
identity : "X86"
}, {
string : navigator.userAgent,
subString : "x64",
identity : "X86_64"
} ],
detectIE : function() {
var ua = window.navigator.userAgent;
var versionSplit = /[\/\.]/i;
var versionRe = /(Version)\/([\w.\/]+)/i; // match for browser version
var operaRe = /(Opera|OPR)[\/ ]([\w.\/]+)/i;
var ieRe = /(?:(MSIE) |(Trident)\/.+rv:)([\w.]+)/i; // must not contain 'Opera'
var match = ua.match(operaRe) || ua.match(ieRe);
if (!match) {
return false;
}
if (Array.prototype.filter) {
match = match.filter(function(item) {
return (item != null);
});
} else { // IE8!
for (var j = 0; j < match.length; j++) {
var matchGroup = match[j];
if (matchGroup == null || matchGroup == '') {
match.splice(j, 1);
j--;
}
}
}
var name = match[1].replace('Trident', 'MSIE').replace('OPR', 'Opera');
var versionMatch = ua.match(versionRe) || match;
var version = versionMatch[2].split(versionSplit);
return {
"name" : name,
"version" : parseFloat(version[0])
};
}
};
BrowserDetect.init();
}
/**
* Mobile Push APIs
*/
var MobilePushApi = (function() {
var channel = null,
mobilePush = {
onPushSucceed: function() { if (console && console.log) console.log("Default onPushSucceed"); },
onRequestChanged: function() { if (console && console.log) console.log("Default onRequestChanged"); },
onApprove: function() { if (console && console.log) console.log("Default onApprove"); },
onDeny: function() { if (console && console.log) console.log("Default onDeny"); },
onError: function() { if (console && console.log) console.log("Default onError"); }
};
/**
* Subscribe to VIP Channel and send a push request to Messaging Services
*
* @param: options
*
* Expected options to be an object. This object may have attribute "additionalPushData" which is another object that will be added to the push data.
* Example: options = { "additionalPushData" : {"showFirstFactor" : true} }
*/
mobilePush.sendPush = function(options){
channel = new MessagingChannel();
channel.onMessage = function(m, f) {
// if mobilePush.onMessage is defined by the client, call this onMessage.
if(typeof mobilePush.onMessage === 'function') {
mobilePush.onMessage(m);
}
else if(!m) {
mobilePush.onError();
}
else {
switch(m.type){
case "can-push":
mobilePush.onPushSucceed(m);
break;
case "request-changed":
mobilePush.onRequestChanged(m);
channel.unsubscribe();
break;
case "ticket":
mobilePush.onApprove(m.ticket);
channel.unsubscribe();
break;
case "denied":
mobilePush.onDeny(m.ticket);
channel.unsubscribe();
break;
case "push-in-progress":
break;
default:
mobilePush.onError(m);
channel.unsubscribe();
}
}
};
channel.onSubscribed = function() {
var data = { type : 'push', user : mobilePush.user, requestId : mobilePush.requestId,
authToken: mobilePush.authToken, authTokenRequestTime: mobilePush.authTokenRequestTime, rpJurHash: mobilePush.rpJurHash};
if (options && options.additionalPushData) {
var additionalPushData = options.additionalPushData;
for(var attr in additionalPushData) {
data[attr] = additionalPushData[attr];
}
}
this.send(data, 'vip-mobile-push');
};
//timeout case
channel.onTimeout = function () {
if (console && console.log) console.log("Request to messaging server timed-out.");
mobilePush.onError("timeout");
}
channel.subscribe();
};
/**
* Unsubscribe the channel. Terminate the long polling connection and exit
*/
mobilePush.cancelPush = function(){
if(channel && typeof channel.unsubscribe() === 'function') {
channel.unsubscribe();
channel = null;
}
};
/**
* A function to set the properties in mobile push credential.
*/
mobilePush.set = function(properties){
for(var property in properties){
mobilePush[property] = properties[property];
}
};
return mobilePush;
})();
/**
* Device Fingerprint APIs
*/
var DeviceFingerprintApi = (function() {
var deviceFingerprint = {
onSuccess : function() { console.log("Default onSuccess"); },
onFailure : function() { console.log("Default onFailure"); },
onError : function() { console.log("Default onError"); }
};
/**
* generateSecurityCode is equivalent genTicket call in inline. It sends a
* Cross Domain Call to iaticket.
* Returns the ticket if the given credential is valid, "Authentication_denied"
* for invalid credential.
*
* reqData example:
* {"fingerPrint":"_v02MyUgNGgYOi88OTk0emB7ZXV9Ajw7MToiJnUbAXVje2R8dRQlJTkwAjA3HjwhemBmYntmY3V9Hh0BGBl5dTk8PjB1EjA2Pjp8dRY9Jzo4MHphZntle2dmYGJ7ZGZndQY0MzQnPHpgZmJ7ZmMpAjw7ZmdzMyUmNmhnYSlkbGdlKWRnZWUpZGxnZSlkZGNlczMlJiJoIjwxMCM8OzA2MTg0MTQlITAnezE5OSk4PT8zNzgxMjYzPzc3JTQwOj86Mzo9OjAzMjwwPT80PCklMCUzOTQmPSU5NCwwJ3sxOTkpPDshMCc7NDl4JzA4OiE8OzJ4IzwwIjAnKTw7ITAnOzQ5eDs0Njl4JTkgMjw7KTw7ITAnOzQ5eCUxM3gjPDAiMCdzMyUlPGgmIjNoZG17ZXtle2VzMyUhL2h4YnMzJTk7aDA7eAAGKSkpczMlNiVoZGRkZGVkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRlZGRkZGVkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRlZGVzMyUjNmhzMyUjJmhnczMlISZoZ21kYWZjbG1gYmxmYWRnczMlITJoYDFkYjQ0MWA0MWcxYTc3ZDQwbTE2MTYzbTcxbWdgM2U=","user":"elmer","isUserIdHashed":false,"requestId":"VIPJS_S0DDG1T0wk8hfFfRRcZKADx9JPTkhd"}
*
*/
deviceFingerprint.generateSecurityCode = function (reqData){
var reqObj = {
_method : 'POST'
},
iaResUrl = ticketServerLocation + '/vipuserservices/resources/iatickets';
if (isLogEnabled) console.log("Calling device fingerprint generateSecurityCode().");
// Use IaDfp.asyncReadFingerprint() when available (iadfp 1.4+, loaded when enhancedDFEnabled=true).
// Falls back to synchronous readFingerprint() for older iadfp versions.
function doCall(fingerPrint) {
if (reqData) {
if (fingerPrint !== undefined) { reqData.fingerPrint = fingerPrint; }
reqData.requestId = symcVipUtils.getRequestId();
reqData.credType = 'IA';
}
reqObj.reqData = JSON.stringify(reqData);
symcVipUtils.crossDomainCalltoVIP(iaResUrl, reqObj, function (data){
// Make sure data is not null, empty, undefined, and data must be an object
if(!data || typeof data != "object") {
return deviceFingerprint.onError(new VIPError({name: 'df', message:'INTERNAL_ERROR'}));
}
if (data.success) {
return deviceFingerprint.onSuccess(data.ret);
} else {
return deviceFingerprint.onFailure(data.ret);
}
});
}
if (typeof IaDfp.asyncReadFingerprint === 'function') {
IaDfp.asyncReadFingerprint().then(function (result) { doCall(result.fingerprint); });
} else {
doCall(IaDfp.readFingerprint());
}
};
/**
* provisioning API
* returns a provsc ticket, and it will write a tag to your browser.
*
* reqData example:
* {"user":"elmer","isUserIdHashed":false,"requestId":"VIPJS_nRTOqHQzaqUVtAmogIrset1fXQIwvu","code":"561944","rememberDevice":"checked","friendlyName":"chris","credType":"IA","browser":"Chrome 43","os":"Windows 7","fingerPrint":"_v02MyUgNGgYOi88OTk0emB7ZXV9Ajw7MToiJnUbAXVje2R8dRQlJTkwAjA3HjwhemBmYntmY3V9Hh0BGBl5dTk8PjB1EjA2Pjp8dRY9Jzo4MHphZntle2dmYGJ7ZGZndQY0MzQnPHpgZmJ7ZmMpAjw7ZmdzMyUmNmhnYSlkbGdlKWRnZWUpZGxnZSlkZGNlczMlJiJoIjwxMCM8OzA2MTg0MTQlITAnezE5OSk4PT8zNzgxMjYzPzc3JTQwOj86Mzo9OjAzMjwwPT80PCklMCUzOTQmPSU5NCwwJ3sxOTkpPDshMCc7NDl4JzA4OiE8OzJ4IzwwIjAnKTw7ITAnOzQ5eDs0Njl4JTkgMjw7KTw7ITAnOzQ5eCUxM3gjPDAiMCdzMyUlPGgmIjNoZG17ZXtle2VzMyUhL2h4YnMzJTk7aDA7eAAGKSkpczMlNiVoZGRkZGVkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRlZGRkZGVkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRlZGVzMyUjNmhzMyUjJmhnczMlISZoczMlITJo","iaTag":null}
*
* rememberDevice boolean
*/
deviceFingerprint.provisionTagAndGenerateSecurityCode = function (reqData, rememberDevice){
var provscResUrl = ticketServerLocation + '/vipuserservices/resources/provsctickets';
var reqObj = {
_method : 'POST'
};
if (isLogEnabled) console.log("Calling device fingerprint provisionTagAndGenerateSecurityCode().");
// Use IaDfp.asyncReadFingerprint() when available (iadfp 1.4+, loaded when enhancedDFEnabled=true).
// Falls back to synchronous readFingerprint() for older iadfp versions.
function doCall(fingerPrint) {
if (reqData) {
if (fingerPrint !== undefined) { reqData.fingerPrint = fingerPrint; }
reqData.requestId = symcVipUtils.getRequestId();
reqData.credType = 'IA';
reqData.browser = PlatformBrowserSupport.getBrowserNameAndVersion();
reqData.os = PlatformBrowserSupport.getOSNameAndArch();
}
reqObj.reqData = JSON.stringify(reqData);
symcVipUtils.crossDomainCalltoVIP(provscResUrl, reqObj, function (data){
if(!data || typeof data !== "object") {
return deviceFingerprint.onError(new VIPError({name: 'df', message:'INTERNAL_ERROR'}));
}
if (data.success) {
// tagging happens on the way back with ticketing response
IaDfp.writeTag(data.ret.credData, rememberDevice); //TODO: write the tag with Account ID.
IaDfp.writeTagId(data.ret.credId);
return deviceFingerprint.onSuccess(data.ret);
} else {
return deviceFingerprint.onFailure(new VIPError({name: 'df', message:'AUTHENTICATION_DENIED'}));
}
});
}
if (typeof IaDfp.asyncReadFingerprint === 'function') {
IaDfp.asyncReadFingerprint().then(function (result) { doCall(result.fingerprint); });
} else {
doCall(IaDfp.readFingerprint());
}
}
/**
* A function to set the properties in device fingerprint credential.
*/
deviceFingerprint.set = function(properties){
for(var property in properties){
deviceFingerprint[property] = properties[property];
}
};
return deviceFingerprint;
})();
/**
* Email Notification APIs
*
* This API provides a method to send a request to the backend to trigger
* an email notification for various user operations (e.g., credential added,
* password changed). It returns a Promise for handling asynchronous results.
*/
var EmailNotificationApi = (function() {
var emailNotification = {};
/**
* Sends a request to the email notification endpoint.
*
* @param {object} reqData - The request data object containing notification details.
* - {string} user: The user's ID.
* - {boolean} isUserIdHashed: Flag indicating if the user ID is hashed.
* - {string} emailAddress: The recipient's email address.
* - {string} operation: The type of operation (e.g., 'CREDENTIAL_ADDED').
* - {string} credentialId: The ID of the credential involved in the operation.
* - {string} credentialType: The type of the credential involved in the operation
* @returns {Promise} A promise that resolves with `true` on success and rejects with an error object on failure.
*/
emailNotification.sendNotification = function(reqData) {
// Return a new Promise. The promise constructor takes a function
// with two arguments: resolve (for success) and reject (for failure).
return new Promise(function(resolve, reject) {
var emailNotificationUrl = ticketServerLocation + '/vipuserservices/resources/emailNotification';
var reqObj = {
_method: 'POST'
};
if (isLogEnabled) console.log("Calling EmailNotificationApi.sendNotification().");
if (reqData) {
reqData.requestId = symcVipUtils.getRequestId();
}
reqObj.reqData = JSON.stringify(reqData);
// Make the cross-domain call. Its callback will now resolve or reject the promise.
symcVipUtils.crossDomainCalltoVIP(emailNotificationUrl, reqObj, function(data) {
// Check for internal errors or malformed responses.
if (!data || typeof data !== "object") {
if (isLogEnabled) console.error("EmailNotificationApi: Received invalid response from server.");
// Reject the promise with an error object.
reject(new VIPError({ name: 'email', message: 'INTERNAL_ERROR' }));
return;
}
// Check the success flag from the backend response.
if (data.success) {
if (isLogEnabled) console.log("EmailNotificationApi: Successfully sent notification request.");
// Resolve the promise, indicating success.
resolve(true);
} else {
if (isLogEnabled) console.error("EmailNotificationApi: Failed to send notification request.", data.ret);
// Reject the promise with the failure data from the server.
reject(false);
}
});
});
};
// Return the public API object.
return emailNotification;
})();
/* =============================================================================
* FILE: vip.js
* ========================================================================== */
var symcVipUtils = (function ($) {
var utils = {};
/**
* Return a Request ID
*/
utils.getRequestId = function (){
var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz';
var requestIdLength = 30;
var prefix = 'VIPJS_';
var requestId = '';
for (var i=0; i 1) {
cookie_value = unescape(a_temp_cookie[1].replace(/^\s+|\s+$/g, ''));
}
// note that in cases where cookie is initialized but no value,
// null is returned
return cookie_value;
break;
}
a_temp_cookie = null;
cookie_name = '';
}
if (!b_cookie_found) {
return null;
}
};
// The canary decision
// if canary threshold is 0 or null or absent, all members go to group B
// if canary threshold is 100, all members go to group A
// return boolean
// expecting {'tdUpgrade' : { 'threshold' : 5 } } as canaryMap
utils.belongToGroupA = function(input, canaryKey, canaryMap) {
var threshold;
if (!canaryKey || !canaryMap || !canaryMap[canaryKey] || !canaryMap[canaryKey].threshold) {
// 0 or null or absent
return false;
}
threshold = canaryMap[canaryKey].threshold;
if (isLogEnabled) console.log('Canary threshold for [' + canaryKey + '] is [' + threshold + '].');
if (canaryKey === 'tdUpgrade') { // this block is specific to tdUpgrade
if (threshold === 100) {
utils.setCookie(canaryKey, '', -1); // reset cookie to '' and expired
return true;
}
var canaryCookieString = utils.getCookie(canaryKey);
if (canaryCookieString === null || canaryCookieString === '') {
if (!input) {
canaryNumber = 0; // put to group A
} else {
// canaryNumber is ALMOST evenly distributed from 0 - 99
// Convert input(ext use id) to lowercase because ext user id is case-insensitive in VIP
canaryNumber = parseInt($.sha256(input.toLowerCase()).substr(0, 4), 16)%100;
}
if (isLogEnabled) console.log('Canary number calculated from input is [' + canaryNumber + '].');
} else {
canaryNumber = parseInt(canaryCookieString)%100;
if (isLogEnabled) console.log('Canary number retrieved from cookie is [' + canaryNumber + '].');
}
// renew the cookie for 365 days, cookie value will be converted from number to string
utils.setCookie(canaryKey, canaryNumber, 365);
return canaryNumber < threshold;
}
};
return utils;
})(vipjQuery);
var vCreds = (function ($) {
var vipCreds = {};
/**
* Utility function for unique suffix
*/
var uniqueSuffix = (function () {
var ts = new Date().getTime();
var suffixSeed = Math.floor(Math.random() * 1000);
return function () {
return ts + '-' + suffixSeed++;
};
})();
var logoutSSP = function (sspUrl, sspToken, requestId) {
var reqObj = {'_method':'DELETE', 'sspToken':sspToken, 'requestId':requestId};
$.getJSON(sspUrl+"?callback=?", reqObj, function (data){
if (isLogEnabled) console.log("logoutSSP() call completed.");
});
};
/**
* constructor for base vip credential
*/
var vipCredCtor = function (name, cType) {
var that = {};
that.getType = function () {
return cType;
};
that.getName = function () {
return name;
};
that.getId = function () {
return name + '-' + uniqueSuffix();
};
that.hasVipTimedOut = function() {
if (!that.genTicketContext) {
if (isLogEnabled) console.log("Warning: hasVipTimedOut called without genTicketContext");
return false;
}
return that.genTicketContext.hasVipTimedOut;
};
that.setVipTimedOut = function(reason) {
if (!that.genTicketContext) {
if (isLogEnabled) console.log("Warning: setVipTimedOut( " +reason + ") called without genTicketContext");
that.genTicketContext = {};
}
that.genTicketContext.hasVipTimedOut = reason;
};
// Default VIP server timeout
that.timeout = 10000;
// The following three functions handle checking / setting genTicketContext.hasVipTimedOut
that.genTicket = function(options, callback) {
that.genTicketContext = options;
if (!that.isFallback && that.hasVipTimedOut()) {
// For the fallbacks, rawGenTicket gets called even if we have previously timed out (though crossDomainCalls will still be skipped)
if (isLogEnabled) console.log("Skipping " + name + " because of previous timeout at " + that.hasVipTimedOut());
return callback(false, new VIPError({name: "timeout", message: "PREVIOUS_TIME_OUT"}));
}
return that.rawGenTicket(options, function(success, err) {
if (!success && Object(err) instanceof VIPError && (err.name == "timeout" || err.message === "REQUEST_TIMEOUT")) {
if (isLogEnabled) console.log(" " + name + " because of previous timeout at " + that.hasVipTimedOut());
that.setVipTimedOut("genTicket:" + name);
}
return callback.apply(this, arguments);
});
};
that.crossDomainCalltoVIP = function(url, data, callback) {
if (that.hasVipTimedOut()) {
if (isLogEnabled) console.log("Skipping crossDomainCalltoVIP(" + url + ") for " + name + " because of previous timeout at " + that.hasVipTimedOut());
// Checking isFallback doesn't make sense here, since even if it is a fallback, calling the already-timed-out VIP server doesn't make sense
return callback(false);
}
return symcVipUtils._crossDomainCalltoVIP({
url: url,
data: data,
timeout: that.genTicketContext.timeout || that.timeout
}, function(ret, err) {
if (!ret && err == "timeout") {
if (isLogEnabled) console.log("Timed out on crossDomainCalltoVIP(" + url + ") for " + name);
that.setVipTimedOut("crossDomainCalltoVIP(" + url + "):" + name);
}
return callback.apply(this, arguments);
});
};
that.watchdog = function(callback, func) {
var timeout = that.genTicketContext.timeout || that.timeout;
if (that.hasVipTimedOut()) {
if (isLogEnabled) console.log("Skipping timed call for " + name + " because of previous timeout at " + that.hasVipTimedOut());
return callback(false, new VIPError({name: "timeout", message: "PREVIOUS_TIME_OUT"}));
}
var wrappedCallback = function(ret, err) {
if (!ret && Object(err) instanceof VIPError && err.name == "timeout") {
if (isLogEnabled) console.log("Watchdog timer expired for " + name);
that.setVipTimedOut("watchdog:" + name);
}
// forward the context and all arguments
return callback.apply(this, arguments);
};
return symcVipUtils.watchdog.call(this, timeout, wrappedCallback, func); // forward the same 'this' the caller used, while using the context / timeout from the credential
};
return that;
};
vipCreds.vipCredCtor = vipCredCtor;
/**
* constructor for Security Code challenge; this is a base creator
*/
var vipChallengeCtor = function (name, spec, cType) {
cType = cType || 'Challenge-Dialog';
var that = vipCredCtor(name, cType);
var metaSel = 'head meta[name="viewport"]';
if ($(metaSel).length === 0 ) {
var meta_name = 'viewport';
var meta_content = 'width=device-width, height=device-height, initial-scale=1.0, maximum-scale=1, user-scalable=yes';
$('').attr('name', meta_name).attr('content', meta_content).appendTo($('head'));
}
// add css for vipbox if not exist; THE FOLLOWING SECTION MAY EXECUTED MULTIPLE TIMES; AVOID DUPLICATION!!!
var rel = 'stylesheet';
if ($("head [title='vip']").length > 0) {
rel = 'alternate stylesheet';
}
var cssURL = ticketServerLocation + "/vipuserservices/static/v_1_0/styles/vipbox.css";
var cssURL2 = ticketServerLocation + "/vipuserservices/static/v_1_0/styles/vipbox2.css";
var cssSel = 'head link[href="' + cssURL + '"]';
if ($(cssSel).length === 0) {
$('').attr('rel', rel).attr('type', 'text/css').attr('title', 'vip-sc-default').attr('href', cssURL).appendTo($('head'));
$('').attr('rel', rel).attr('type', 'text/css').attr('title', '').attr('href', cssURL2).appendTo($('head'));
}
/* For Quirks Mode check for compact mode*/
if (document.compatMode == "BackCompat") {
var qcssURL = ticketServerLocation + "/vipuserservices/static/v_1_0/styles/vipbox-quirks.css";
var qcssSel = 'head link[href="' + qcssURL + '"]';
if ($(qcssSel).length === 0) {
$('').attr('rel', rel).attr('type', 'text/css').attr('title', 'vip-sc-default-quirks').attr('href', qcssURL).appendTo($('head'));
}
}
var overlayConf = spec.overlayConf || {
top: 260,
mask: {
color: '#cfcfcf',
loadSpeed: 200,
closeSpeed: 0,
opacity: 0.5,
onBeforeLoad: function() {
// check existing div, insert only if there is no div there.
if($("#exposeMask div").length==0){
$("").appendTo("#exposeMask");
}
}
},
closeOnClick: false,
load: true
};
var callbackOnOverlayClose = null;
that.showOverlay = function (element) {
// README: potential duplicate removal; but for now it is harmless; review later; move to closeOverlay()?
overlayConf.onClose = function () {element.remove();};
var isMobile = /iPhone|iPod|Android/i.test(navigator.userAgent);
if (isMobile) {
if (isLogEnabled) console.log('Device connected is Mobile');
element.overlay(overlayConf).css({
'position': 'fixed',
'top': '0px',
'left': '0px',
'width': '100%',
'height': '100%',
'margin-left': '0px',
'margin-top': '0px',
'overflow': 'auto',
'z-index': 9999,
'box-shadow': '0px 0px 20px rgba(29, 17, 86, .3)',
'border': 'none',
'display': 'block'
});
} else {
if (isLogEnabled) console.log('Device connected is Desktop');
element.overlay(overlayConf).css({
'width': '400px',
'height': '540px',
'top': '30%',
'left': '50%',
'margin-left': '-220px',
'margin-top': '-125px',
'position': 'absolute',
'overflow': 'auto',
'z-index': 9999,
'box-shadow': '0px 0px 20px rgba(29, 17, 86, .3)',
'border': 'none',
'display': 'block'
});
}
};
that.closeOverlay = function(element) {
element.data("overlay").close();
if (callbackOnOverlayClose)
callbackOnOverlayClose();
};
that.onOverlayClose = function(callback) { // register a callback upon overlay close; is this a hack?
callbackOnOverlayClose = callback;
};
that.logoutSSP = logoutSSP;
return that;
};
/**
* constructor for TrustBearer credential
*/
vipCreds.rc = function (name, spec, cType) {
cType = cType || "TRUSTBEARER";
var that = vipChallengeCtor(name, spec, cType);
if (!window.rcInitializer || !window.tdInitializer) {
that.genTicket = function (options, callback) {
throw new VIPError({name: 'rc', message: 'INIT_FAILED'});
};
}
else {
/*
To account for the interface differences, promise API upgrade, and easy removal of RC 1.12, we load "td" rc or "1.12" rc
based on policy and supported platform.
A canary decision is added in. Group A will be upgraded, and default is group A.
*/
var rcWrapper = vipChallengeCtor(name, spec, cType); var tdWrapper = vipChallengeCtor(name, spec, cType);
window.rcInitializer.call(rcWrapper, spec);
window.tdInitializer.call(tdWrapper, spec);
that.rawGenTicket = function(options, callback) {
var isTrustedDeviceEnabled = (window.isTrustedDeviceEnabled === 'true');
if (PlatformBrowserSupport.isTrustedDeviceSupported() && isTrustedDeviceEnabled
&& symcVipUtils.belongToGroupA(options.user, 'tdUpgrade', vipCreds.policy.canaryThresholds)) {
//put the canary check at the last to minimize the side effect
tdWrapper.genTicket(options, callback);
} else {
rcWrapper.genTicket(options, callback);
}
};
}
return that;
};
/**
* constructor for OTP
*/
vipCreds.otp = function (name, spec, cType) {
cType = cType || 'OTP-Dialog';
var that = vipChallengeCtor(name, spec, cType);
var htmlStr = spec.contentHtml || ('
' +
'
Confirm Your Identity
' +
'
' +
'' +
'
' +
'' +
'
' +
'' +
'
' +
'' +
''+
'
' +
'
' +
'' +
'
' +
'
');
that.isFallback = true;
that.rawGenTicket = function (options, callback) {
if (isLogEnabled) console.log("Calling otp.gentTicket().");
// add a new div to document body
var divId = that.getId();
// check duplicate div, generate a new Id for this div
while($(divId).length>0){
divId = that.getId();
}
$("", {
"id" : divId,
"class" : "symc",
html: htmlStr
}).appendTo("body");
var otpField = $("#" + divId + " #otpInput");
// add the callback into the event handler
var retrieveOTP = function () {
callback(true, {ticket:otpField.val()});
};
var cancelOTP = function () {
callback(false, new VIPError({name: 'otp', message:'OPERATION_CANCELLED'}));
};
// display overlay
$("#" + divId + " #vipSubmitOTP").click(retrieveOTP);
$("#" + divId + " #vipCancelOTP").click(cancelOTP);
/*
* Capture of Enter Key for Enter Security Code
* - FF5.0 Bug when submitting the form on OOB page.
* Whole pages gets submitted. Comment the code for now.
*/
/*
$("#otpInput").on('keyup', function(e) {
if(e.keyCode==13){
$("#" + divId + " #submitOTP").click();
}
});
*/
that.showOverlay($("#" + divId));
};
return that;
};
/**
* constructor for IA
*/
vipCreds.ia = function (name, spec, cType) {
cType = cType || 'IA';
var that = vipCredCtor(name, cType);
var iaResUrl = ticketServerLocation + '/vipuserservices/resources/iatickets';
var reqObj = {'_method':'POST'};
that.rawGenTicket = function (options, callback) {
if (isLogEnabled) console.log("Calling ia.genTicket().");
// Use IaDfp.asyncReadFingerprint() when available (iadfp 1.4+, loaded when enhancedDFEnabled=true).
// Falls back to synchronous readFingerprint() for older iadfp versions (1.0/1.1).
// iadfp 1.4 is loaded in place of 1.1 for enhancedDFEnabled=true requests; existing 1.1
// users will see a one-time device tag mismatch due to the richer 1.4 fingerprint format.
function doCall(fingerPrint) {
reqData = {};
if (fingerPrint !== undefined) { reqData.fingerPrint = fingerPrint; }
reqData.user = options.user;
reqData.isUserIdHashed = !!options.isUserIdHashed;
reqData.requestId = options.requestId;
reqObj.reqData = JSON.stringify(reqData);
that.crossDomainCalltoVIP(iaResUrl, reqObj, function (data){
// Make sure data is not null, empty, undefined, and data must be an object
if(!data || typeof data != "object") {
return callback(false, new VIPError({name: 'ia', message:'INTERNAL_ERROR'}));
}
if (data.success) {
callback(data.success, data.ret);
} else { // IA reject request, ticket not generated, fallback to next step
options.secureRandom = data.ret.secureRandom;
options.showRememberDevice = data.ret.showRememberDevice;
options.status = data.status;
callback(false, new VIPError({name: 'ia', message:'AUTHENTICATION_DENIED'}));
}
});
}
if (!options.iaExcludeDfp) {
if (typeof IaDfp.asyncReadFingerprint === 'function') {
IaDfp.asyncReadFingerprint().then(function (result) { doCall(result.fingerprint); });
} else {
doCall(IaDfp.readFingerprint());
}
} else {
doCall();
}
};
return that;
};
/**
* constructor for IASC
*/
vipCreds.iasc = function (name, spec, cType) {
cType = cType || 'IA-SC';
var that = vipChallengeCtor(name, spec, cType);
that.isFallback = true;
var htmlStr = spec.contentHtml || ('
'+
''
);
var genTicketInternal = function (options, callback, otp, rememberDevice, divId) {
var iascResUrl = ticketServerLocation + '/vipuserservices/resources/iasctickets';
var reqObj = {'_method':'POST'};
rememberDevice = rememberDevice || false;
// Use IaDfp.asyncReadFingerprint() when available (iadfp 1.4+, loaded when enhancedDFEnabled=true).
// Falls back to synchronous readFingerprint() for older iadfp versions (1.0/1.1).
// iadfp 1.4 is loaded in place of 1.1 for enhancedDFEnabled=true requests; existing 1.1
// users will see a one-time device tag mismatch due to the richer 1.4 fingerprint format.
function doCall(fingerPrint) {
var reqData = {};
if (fingerPrint !== undefined) { reqData.fingerPrint = fingerPrint; }
reqData.user = options.user;
reqData.isUserIdHashed = !!options.isUserIdHashed;
reqData.requestId = options.requestId;
reqData.code = otp;
reqData.rememberDevice = rememberDevice;
reqObj.reqData = JSON.stringify(reqData);
that.crossDomainCalltoVIP(iascResUrl, reqObj, function (data){
// Make sure data is not null, empty, undefined, and data must be an object
if(!data || typeof data != "object") {
return callback(false, new VIPError({name: 'iasc', message:'INTERNAL_ERROR'}));
}
var vipBox = $("#" + divId);
if (vipBox.length > 0) { // proceed when vipBox is not closed
that.closeOverlay(vipBox);
if (data.success) {
if (rememberDevice && !options.iaExcludeDfp) { // do tagging with the response
IaDfp.writeTag(data.ret.credData, rememberDevice);
IaDfp.writeTagId(data.ret.credId);
}
callback(data.success, data.ret);
} else {
callback(false, new VIPError({name: 'iasc', message:'AUTHENTICATION_DENIED'}));
}
}
});
}
if (!options.iaExcludeDfp) {
if (typeof IaDfp.asyncReadFingerprint === 'function') {
IaDfp.asyncReadFingerprint().then(function (result) { doCall(result.fingerprint); });
} else {
doCall(IaDfp.readFingerprint());
}
} else {
doCall();
}
};
var createSspFrame = function (divId, egUrl, username, password, sspToken) {
var sspFrameContainer = $("#" + divId + " #vipSspFrameContainer");
var neverFired = true;
var populateSspFrame = function () {
if (neverFired) {
neverFired = false; // stop firing again!!! (In the local domain case, almost never happen in real world)
var body = $(this).contents().find("body"); // In IE6 .find() returns an object that .appendTo() is not expecting. javascript error "Invalid Argument". Thus changed it to use .append() for next statement
body.append("");
var form = $(this).contents().find("form");
form.append("");
form.append("");
// use .val() function to handle single/double quote escape so that we don't need to do it by hand.
form.find("input[name='username']").val(username);
form.find("input[name='password']").val(password);
form.append("");
form.submit();
}
};
$('').on('load', populateSspFrame).appendTo(sspFrameContainer);
};
var getOOBOptions = function (divId, options, egUrl, sspUrl) {
var sspToken = (options.secureRandom + options.user).substr(0, 32);
// add an EG iframe to signon to SSP, use loginID instead of user because user maybe a hash
// loginID is the value entered by the user.
createSspFrame(divId, egUrl, options.loginID, options.password, sspToken);
var reqObj = {'_method':'GET'};
reqObj.sspToken = sspToken;
var timeout = 500;
var retryCnt = 8;
var getOOB = function () {
$.getJSON(sspUrl+"?callback=?", reqObj, function (data){
//console.log(data);
if ((data !== null && typeof data === 'object' && data.success === true)) {
// remove the EG iframe
$("#" + divId + " #vipSspFrameContainer #sspFrame").remove();
// display ooblink
$("#" + divId + " #vipOoblink").css("display", "block");
$("#otpInput").focus();
var oobList = data.ret;
var oobDiv = $("#" + divId + " #vipSspFrameContainer #vipOobDiv #vipOobForm #vipOobOptions");
var radioselc = 0;
for (name in oobList) {
oobDiv.append('
';
/***************** Views ********************/
var ProvView = Backbone.View.extend({
tagName: "div",
className: "symc2",
template: _.template(underi18n.template(templateOuter, translate)),
events: {
"click #vipCancelOTP": "cancel",
"click #vipCancelsubmitOTP": "cancel"
},
// The View listens for changes to its model, re-rendering.
initialize: function() {
this.model.bind('change:message', this.showMessage, this);
this.notAdded = true;
this.subViewMap = {};
},
createOOBFrameAndGetOOB : function() {
var that = this;
var divId = this.id;
//README: To enable SAML, the idpUrl must NOT be null, but password can be null
var getOOBOptionsResult = function(result) {
$("#vipSspProgressDiv").css("display","none");
if (!result) {
that.closeAndCallback(false, new VIPError({name: 'provsc', message:'OOB_TIMEOUT'}));
}else if(that.model.get("navigateOOBScreen")){
that.showNavigateOOB();
}else
{
that.renderSubView(SCView);
}
}
if(idpUrl) {
// if IDP failure, fall back to SC mode
setTimeout(function() {
that.renderSCView(that);
}, 15000);
getOOBOptions(divId, this.options.env.options, idpUrl, sspUrl, getOOBOptionsResult);
}else{
$("#vipSspProgressDiv").css("display","none");
that.renderSubView(SCView);
}
},
renderSCView: function(ref) {
if($("#vipSspProgressDiv").css('display')=='block'){
if (isLogEnabled) console.log("WARNING: IDP url is not responding in time, fallback to SC.");
$("#vipSspProgressDiv").css("display","none");
ref.renderSubView(SCView);
}
},
showNavigateOOB: function() {
this.model.set("message", null); //reset the message
this.renderSubView(OOBView);
},
close: function() {
this.model.unbind('change:message', this.showMessage, this);
if (this.model.get("sspToken")) {
this.options.env.context.logoutSSP(sspUrl, this.model.get("sspToken"), this.model.get("requestId"));
}
if (!this.notAdded) {
this.$el.data("overlay").close();
this.renderSubView(null); // cleanup the subview
}
this.notAdded = true;
this.remove();
},
closeAndCallback: function() {
this.close();
this.options.env.callback.apply(null, arguments);
},
cancel: function(){
this.closeAndCallback(false, new VIPError({name: 'provsc', message:'OPERATION_CANCELLED'}));
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
if (this.notAdded) {
// Intercept and Handle Mobile Push Approved Initiated Provisioning.
if (this.model.get("mobilePushApproved") === true) {
if (this.model.isRememberDevice()) {
if (isLogEnabled) console.log("Mobile Push Approved, RD allowed. Entering MD-View.");
this.$el.appendTo("body");
this.renderSubView(MDView);
this.options.env.context.showOverlay(this.$el);
this.notAdded = false;
}
// we shortcircuit here to skip the ticketing. no IA, no provisoning, just return the mobile Push Approved Ticket.
else if (!decideIApresent()) {
if (isLogEnabled) console.log("Mobile Push Approved, No RD allowed and No IA. Submitting ticket as-is.");
this.closeAndCallback(true, {ticket: this.model.get("mobilePushTicket")});
}
// give feedback to IA
else {
if (isLogEnabled) console.log("Mobile Push Approved, IA present. Submitting ticket via provSC for IA feedback.");
var that = this;
var callback = function (data) {
that.closeAndCallback.apply(that, arguments);
};
createTicket(this.model, callback);
}
}
else {
this.$el.appendTo("body");
// Added for MO_FALLBACK_OTP policy
if (isMOFallbackEnabled === 'false') {
if (isLogEnabled) console.log("ProvSC - MO Fallback is disabled");
this.renderSubView(MOFallbackView);
} else {
// this.renderSubView(SCView);
}
this.options.env.context.showOverlay(this.$el);
this.notAdded = false;
}
}
//This is called after the template is rendered because iFrame will be added it into the template
if (this.model.get("mobilePushApproved") !== true) {
if (isLogEnabled) console.log("Fetching OOB Options from SSP.")
$("#vipSspProgressDiv").css("display","block");
this.createOOBFrameAndGetOOB();
}
if (this.options.env.options.postRendering && typeof this.options.env.options.postRendering === 'function') {
this.options.env.options.postRendering(this);
}
return this;
},
renderSubView: function(newSubview) {
if (this.options.subview) {
this.options.subview.unbind();
this.options.subview.remove();
}
if (newSubview) {
this.options.subview = new newSubview({model: this.model, parentView: this, env: this.options.env}).render();
this.showTitle(translate(this.options.subview.title));
}
},
subViewEl: function() {
var el = this.$("#subview");
if (!el.length)
return $("
", {id: "subview"}).appendTo(this.$("#vipSubviewContainer"));
else
return el;
},
showMessage: function() {
var message = this.model.get("message");
if (message) {
this.$("#vipMessageBox").removeClass().addClass(message.type);
this.$("#vipMessageContent").text(message.text);
} else {
this.$("#vipMessageBox").removeClass();
this.$("#vipMessageContent").text("");
}
return this;
},
showTitle: function(title) {
this.$("#vipDialogTitle").text(title);
}
});
// Added for MO_FALLBACK_OTP policy
var MOFallbackView = Backbone.View.extend({
title: "",
template: _.template(underi18n.template(templateMOFallback, translate)),
initialize: function() {
this.setElement(this.options.parentView.subViewEl());
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
var SCView = Backbone.View.extend({
title: "",
template: _.template(underi18n.template(templateSC, translate)),
events: {
"click #vipSubmitOTP": "next",
"click #vipRememberDevice": "remember",
"click #vipOoblink" : "showOOB",
"keypress #otpInput" : "onKeyPress"
},
onKeyPress: function(e){
var code = e.keyCode || e.which;
if ( code === 13 ) {
this.next();
}
},
initialize: function() {
this.setElement(this.options.parentView.subViewEl());
this.model.on("change:sspToken",
function() {
this.model.set("rememberDevice", this.model.get("rememberDeviceDefault"));
if (this.model.get("rememberDevice")) {
this.model.set("credType", this.whichProvCredType());
} else {
this.model.set("credType", null);
}
},
this);
this.model.on("change:policy",
function() {
this.render();
this.showRememberDevice();
},
this);
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
this.showRememberDevice();
if (this.options.env.options.postRendering && typeof this.options.env.options.postRendering === 'function') {
this.options.env.options.postRendering(this);
}
if (this.options.env.options.unSupportedPlatformForPlugin === true) {
var isMobile = /iPhone|iPod|Android/i.test(navigator.userAgent);
if (!isMobile) {
this.model.set("message", {
type: "error",
text: translate("message.error.unsupported.platform")
});
}
}
//theModel.set("message", {type:"success", text: translate("message.success.send.securitycode", {decodedOption: 1234})});
theOption = this.options;
return this;
},
showRememberDevice: function() {
if (!this.isRememberOptionDisplayed()) {
this.$("#vipRememberDeviceDiv").hide();
} else {
this.$("#vipRememberDeviceDiv").show();
}
if (this.model.get("forceRememberDeviceDisplay") === "on-ro") {
this.$("#vipRememberDevice").prop("disabled", true);
}
// disable the button ONLY if max allowed is reached
var policy = theModel.get("policy");
// check policy.RD_ALLOW === false implies checking the property exist and the value is boolean type false
if (policy && typeof policy === "object" && policy.RD_ALLOW === false){
if (isLogEnabled) console.log("RD_ALLOW is false, disable the checkbox");
this.$("#vipRememberDevice").prop("checked", false);
this.$("#vipRememberDevice").prop("disabled", true);
this.$("#rememberDeviceLabel").addClass("labelDisabled");
}
},
remember: function() {
var isRememberDeviceChecked = (this.$("#vipRememberDevice").prop('checked'));
this.model.set("rememberDevice", isRememberDeviceChecked);
if (isRememberDeviceChecked) {
this.model.set("credType", this.whichProvCredType());
} else {
this.model.set("credType", null);
}
},
isRememberOptionDisplayed : function() {
var forceRememberDeviceDisplay = this.model.get("forceRememberDeviceDisplay");
if (typeof forceRememberDeviceDisplay === 'string' && forceRememberDeviceDisplay.indexOf("on") === 0) return true;
if (typeof forceRememberDeviceDisplay === 'string' && forceRememberDeviceDisplay.indexOf("off") === 0) return false;
// if the platform does not support RC and IA is not present in the initCredArray or df is disabled, do no show Remember Me
if(decideProvCredType(this.model.get("upgradeSkipped")) === 'IA' && (!decideIApresent() || this.options.env.options.iaExcludeDfp)) return false;
if(decideProvCredType(this.model.get("upgradeSkipped")) === 'RC' && !decideRCpresent()) return false;
if (!this.model.get("sspToken")) return false;
return true;
},
whichProvCredType: function() {
// give a chance for overriding the credential type at the runtime
var credTypeFromOptions = this.options.env.options.provCredType;
return credTypeFromOptions || decideProvCredType(this.model.get("upgradeSkipped"));
},
next: function() {
var securityCode = this.$("#otpInput").val().trim();
var pattern = new RegExp("^([0-9])*$");
if(!securityCode || securityCode.length < 6 || !pattern.test(securityCode)){
this.model.set("message", {type:"error", text:translate("message.error.invalid.securitycode")});
return;
} else {
this.model.set("securityCode", securityCode);
}
this.model.set("message", null); //reset the message
if (cred.hasVipTimedOut()) {
// We have already timed out attempting to connect with the VIP services, so short-circuit to calling back to the RP
this.options.parentView.closeAndCallback(true, {ticket: this.model.get("securityCode")});
} else if (this.model.get("rememberDevice")) {
//this.options.parentView.renderSubView((this.model.get("credType") === "RC" ? DLView : FNView));
if(this.model.get("credType") !== "RC") {
this.options.parentView.renderSubView(FNView);
}
else{
if(
BrowserDetect.browser == "Chrome"
|| (BrowserDetect.browser == "Firefox" && this.options.env.options.jsVersion === "1_16")
/* Safari >= 12 uses an extension, however is only supported for >= 1_16 */
|| (BrowserDetect.browser === "Safari" && BrowserDetect.version >= 12 && this.options.env.options.jsVersion === "1_16")
){
theModel.extensionExistCheck();
} else {
theModel.pluginExistCheck();
}
}
}else if (!decideIApresent()){
// we shortcircut here to skip the ticketticketing. no IA, no provisoning, just return the security code.
this.options.parentView.closeAndCallback(true, {ticket: this.model.get("securityCode")});
} else {
var parent = this.options.parentView;
var callback = function(data) {
parent.closeAndCallback.apply(parent, arguments);
};
createTicket(this.model, callback);
}
},
showOOB: function() {
this.model.set("message", null); //reset the message
this.options.parentView.renderSubView(OOBView);
}
});
var MDView = Backbone.View.extend({
title: "",
template: _.template(underi18n.template(templateMD, translate)),
events: {
"click #vipSkipBtn": "skip",
"click #vipNext": "next"
},
initialize: function() {
this.setElement(this.options.parentView.subViewEl());
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
if (this.options.env.options.postRendering && typeof this.options.env.options.postRendering === 'function') {
this.options.env.options.postRendering(this);
}
theOption = this.options;
return this;
},
next: function() {
console.log("ProvSC Parent View : Mobile Push Approved, attempt to provision device of type " + this.whichProvCredType());
this.model.set("rememberDevice", true);
this.model.set("credType", this.whichProvCredType());
if (this.model.get("credType") !== "RC") {
this.options.parentView.renderSubView(FNView);
}
else {
if(
BrowserDetect.browser == "Chrome"
|| (BrowserDetect.browser == "Firefox" && this.options.env.options.jsVersion === "1_16")
/* Safari >= 12 uses an extension, however is only supported for >= 1_16 */
|| (BrowserDetect.browser === "Safari" && BrowserDetect.version >= 12 && this.options.env.options.jsVersion === "1_16")
){
theModel.extensionExistCheck();
} else {
theModel.pluginExistCheck();
}
}
},
skip: function () {
if (!decideIApresent()) {
// we shortcircut here to skip the ticketticketing. no IA, no provisoning, just return the security code.
this.options.parentView.closeAndCallback(true, {ticket: this.model.get("mobilePushTicket")});
} else {
var parent = this.options.parentView;
var callback = function (data) {
parent.closeAndCallback.apply(parent, arguments);
};
createTicket(this.model, callback);
}
},
whichProvCredType: function() {
// give a chance for overriding the credential type at the runtime
var credTypeFromOptions = this.options.env.options.provCredType;
return credTypeFromOptions || decideProvCredType(this.model.get("upgradeSkipped"));
}
});
//Handle Remembered Device Over Max Limit Blocked by Admin Policy
var RDBlkView = Backbone.View.extend({
title: "",
template: _.template(underi18n.template(templateRDBlkd, translate)),
events: {
"click #vipNext": "next"
},
initialize: function() {
this.setElement(this.options.parentView.subViewEl());
},
render: function() {
var policy = theModel.get("policy");
policy.RD_MAX_ALLOWED = this.options.env.options.rdLimit;
this.$el.html(this.template(this.model.toJSON()));
if (this.options.env.options.postRendering && typeof this.options.env.options.postRendering === 'function') {
this.options.env.options.postRendering(this);
}
theOption = this.options;
return this;
},
next: function () {
if (!decideIApresent()) {
// we shortcircut here to skip the ticketticketing. no IA, no provisoning, just return the security code.
this.options.parentView.closeAndCallback(true, {ticket: this.model.get("mobilePushTicket")});
} else {
var parent = this.options.parentView;
var callback = function (data) {
parent.closeAndCallback.apply(parent, arguments);
};
createTicket(this.model, callback);
}
}
});
var OOBView = Backbone.View.extend({
title: "",
template: _.template(underi18n.template(templateOOB, translate)),
events: {
"click #vipBack": "back",
"click #vipSend": "send"
},
initialize: function() {
this.setElement(this.options.parentView.subViewEl());
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
this.renderOOB();
if (this.options.env.options.postRendering && typeof this.options.env.options.postRendering === 'function') {
this.options.env.options.postRendering(this);
}
return this;
},
back: function() {
this.options.parentView.renderSubView(SCView);
},
send: function() {
this.sendOOB();
this.options.parentView.renderSubView(SCView);
},
renderOOB: function() {
var oobDiv = $("#" + this.options.parentView.id + " #vipSspFrameContainer #vipOobDiv #vipOobForm #vipOobOptions");
var oobList = this.model.get('oobList');
for (name in oobList) {
var oobString2 = "';
oobDiv.append(oobString2);
}
$('[name="oob"]').first().attr('checked','checked');
},
sendOOB: function() {
var divId = this.options.parentView.id;
var oobDiv = $("#" + divId + " #vipSspFrameContainer #vipOobDiv #vipOobForm #vipOobOptions");
var selected = oobDiv.find('input[name="oob"]:checked').val();
var vOptionSel = theModel.get('oobList')[selected];
var decodedOption = $(document.createElement('div')).html(vOptionSel).text();
var reqObj = {'_method':'PUT', 'opr':selected, 'sspToken':theModel.get('sspToken'), 'requestId':theModel.get('requestId')};
$.getJSON(sspUrl+"?callback=?", reqObj, function (data){
if ((data !== null && typeof data === 'object' && data.success === true)) {
theModel.set("message", {type:"success", text: translate("message.success.send.securitycode", {decodedOption: decodedOption})});
} else {
var errorMsg = "";
if (data != null)
errorMsg = data.ret.errorMsg;
var allPresent = false;
if (errorMsg != null || errorMsg.trim() != "") {
var keywords = ["sms", "try", "minute"];
allPresent = keywords.every(function(keyword) {
return errorMsg.toLowerCase().indexOf(keyword.toLowerCase()) !== -1;
});
}
if (allPresent) {
theModel.set("message", {
type: "error",
text: errorMsg
});
} else {
theModel.set("message", {
type: "error",
text: translate("message.error.send.securitycode", {decodedOption: decodedOption})
});
}
}
});
}
});
var EDView = Backbone.View.extend({
title: "",
template: _.template(underi18n.template(templateED, translate)),
events: {
"click #vipNext": "download",
"click #refreshSafari": "refresh",
"click #retry": "download",
"click #retrySafari": "download",
"click #vipSkipbtn": "skip", //TODO add skip function
"click #skip": "skip"
},
initialize: function() {
this.setElement(this.options.parentView.subViewEl());
/* Reset the extension installation processed state */
theModel.set("extensionInstallProcessed", false);
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
this.$("input#vipNext").prop('disabled', true);
if (this.options.env.options.postRendering && typeof this.options.env.options.postRendering === 'function') {
this.options.env.options.postRendering(this);
}
/* Update the state of the panel */
this.$("#vipExtensionStepDiv").addClass("pending").removeClass("complete");
this.$("#vipPluginStepDiv").addClass("pending").removeClass("complete");
this.$("input#vipNext").prop('disabled', false);
/* Mark the flow as having passed through installation flow */
theModel.set("installProcessed", true);
/* Now visible - start the extension present check */
waitForExtension().then(function() {
/* If already processed, do nothing else */
if (theModel.get("extensionInstallProcessed")) { return; }
theModel.set("extensionInstallProcessed", true);
console.debug("Found extension, continuing process.");
/* Update the state - moving to next flow */
theModel.set("extExist", true);
})["catch"](function(err) {
/* If already processed, do nothing else */
if (theModel.get("extensionInstallProcessed")) { return; }
/* Not an expected error due to infinite timeout */
theModel.set("extensionInstallProcessed", true);
console.error("Error waiting on extension:", err);
return callback(false, err);
});
return this;
},
download: function() {
if(window.console && window.console.debug) window.console.debug("Install Chrome, Firefox, or Safari Extension...");
this.showInstallerInstruction();
/* If Firefox - use different setup */
if (BrowserDetect.browser == "Firefox") {
/* Use the firefox method to do the install */
pkiClientExtension.firefoxInstaller.install().then(function() {
if(tbVip.doLog) console.info("Extension installed per Firefox flow - awaiting detection");
})["catch"](function(err) {
if(tbVip.doLog) console.error("Error installing Firefox extension:", err);
return callback(false, err);
});
} else if (BrowserDetect.browser === "Safari" && BrowserDetect.version >= 12) {
/* Open it in a new window just in case file processing fails and there's an error page */
window.open(SAFARI_EXTENSION_DOWNLOAD_URL, '_blank');
} else if (BrowserDetect.browser === "Chrome") {
/* Use Chrome mechanism */
var url = "https://chrome.google.com/webstore/detail/" + pkiClientExtension.getChromeID();
/* Launch webstore URL to start extension installation */
var installWindow = window.open(url, '_blank');
var __this = this;
/* Monitor the window to ensure that the user didn't close it w/o installing.
* NOTE: It will not catch the user deciding to use the window for some other purpose than installation.
* NOTE: It would also be possible to close the extension window once it is installed, but if the user
* did not expect this, it could be undesirable.
*/
var installWindowWatcher = setInterval(function() {
if (theModel.get("extensionInstallProcessed")) {
if (installWindowWatcher != null) {
clearInterval(installWindowWatcher);
installWindowWatcher = null;
}
return;
}
if (installWindow.closed) {
console.debug("User closed the extension window without installing");
clearInterval(installWindowWatcher);
installWindowWatcher = null;
}
}, 500);
console.debug("installExtensionClickHandler(): Page opened, waiting for extension...");
} else {
if(tbVip.doLog) console.error("Unhandled browser type+version:" + BrowserDetect.browser + ":" + BrowserDetect.version);
callback(false, "UnhandledBrowser");
}
},
refresh: function() {
/* Refresh the page in response to Safari special case */
window.location.reload();
},
skip: function() {
if (!decideIApresent()){
if (this.model.get("mobilePushApproved")){
this.options.parentView.closeAndCallback(true, {ticket: this.model.get("mobilePushTicket")});
}
// we shortcircut here to skip the ticketing. no IA, no provisoning, just return the security code.
if (this.model.get("securityCode")) {
this.options.parentView.closeAndCallback(true, {ticket: this.model.get("securityCode")});
}
} else {
var parent = this.options.parentView;
this.model.set('rememberDevice', false);
this.model.set('credType', null);
var callback = function(data) {
parent.closeAndCallback.apply(parent, arguments);
};
createTicket(this.model, callback);
}
},
showInstallerInstruction: function() {
if (BrowserDetect.browser == "Firefox" && this.options.env.options.jsVersion === "1_16"){
$('.vipExtensionDiv').hide();
$('#vipFirefoxExtensionInstructionDiv').show();
$('#vipRetryDiv').show();
} else if (BrowserDetect.browser === "Safari" && BrowserDetect.version >= 12 && this.options.env.options.jsVersion === "1_16") {
/* NOTE: Not showing retry due to it bundled specially inside the noraml instruction set */
$('.vipExtensionDiv').hide();
$('#vipSafariExtensionInstructionDiv').show();
/* TODO / HACK / FIXME: This is an extension of the dialog by 100px based on known value so
* that the new content fits. This will definitely need an adjustment when localized as
* text will be longer.
*/
this.options.parentView.$el.css('height', '640px');
}
}
});
var DLView = Backbone.View.extend({
title: "",
template: _.template(underi18n.template(templateDL, translate)),
events: {
"click #vipNext": "download",
"click #retry": "download",
"click #vipSkipbtn": "skip",
"click #skip": "skip"
},
initialize: function() {
this.setElement(this.options.parentView.subViewEl());
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
this.$("input#vipNext").prop('disabled', true);
if (this.options.env.options.postRendering && typeof this.options.env.options.postRendering === 'function') {
this.options.env.options.postRendering(this);
}
/* Update the state of the panel */
this.$("#vipExtensionStepDiv").addClass("complete").removeClass("pending");
this.$("#vipPluginStepDiv").addClass("pending").removeClass("complete");
/* Mark the flow as having passed through installation flow */
theModel.set("installProcessed", true);
window.rcInstallation(theModel.get("sspToken"), function(success, devId){
//var time = new Date();
//console.log("DEBUG: rcInstallation finish: " + time.getMinutes() + " : " + time.getSeconds());
if (success) {
theModel.set("deviceId", devId);
theModel.credExistCheck();
} else {
theModel.set("message", {type:"error", text: translate("message.error.install.plugin")});
}
}, this, this.enableDownload);
return this;
},
download: function() {
this.$("input#vipNext").prop('disabled', true);
if (isLogEnabled) console.log('Skip prefly check for sspToken. Download the file now.');
this.createFrameAndDownload();
this.showInstallerInstruction();
},
createFrameAndDownload: function(){
var filename = window.rcInstallerFile();
var downloadUrl = ticketServerLocation + "/vipuserservices/resources/plugin/" + filename;
$('').appendTo(this.$el);
window.open(downloadUrl, 'downloadFrame');
},
skip: function() {
if (!decideIApresent()){
// we shortcircut here to skip the ticketing. no IA, no provisoning, just return the security code.
if (this.model.get("mobilePushApproved")){
this.options.parentView.closeAndCallback(true, {ticket: this.model.get("mobilePushTicket")});
}
if (this.model.get("securityCode")) {
this.options.parentView.closeAndCallback(true, {ticket: this.model.get("securityCode")});
}
} else {
var parent = this.options.parentView;
this.model.set('rememberDevice', false);
this.model.set('credType', null);
var callback = function(data) {
parent.closeAndCallback.apply(parent, arguments);
};
createTicket(this.model, callback);
}
},
showInstallerInstruction: function() {
$('.vipDownloadDiv').hide();
if(BrowserDetect.browser.indexOf("Explorer")!=-1){
$('#vipIeInstructionDiv').show();
}
else if(BrowserDetect.browser.indexOf("Firefox")!=-1) {
$('#vipFfInstructionDiv').show();
}
else if(BrowserDetect.browser.indexOf("Chrome")!=-1) {
$('.vipChromeInstructionDiv').show();
}
else if(BrowserDetect.browser.indexOf("Safari")!=-1) {
$('#vipSafariInstructionDiv').show();
}
$('#vipRetryDiv').show();
},
enableDownload: function() {
if (isLogEnabled) console.log('RC not found, enabling download.');
this.$("input#vipNext").prop('disabled', false);
}
});
var FNView = Backbone.View.extend({
title: "",
template: _.template(underi18n.template(templateFN, translate)),
events: {
"click #vipContinue": "confirmName",
"click #vipNext": "next"
},
initialize: function() {
this.setElement(this.options.parentView.subViewEl());
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
if (this.options.env.options.postRendering && typeof this.options.env.options.postRendering === 'function') {
this.options.env.options.postRendering(this);
}
/* Check if the panel should be visible in this flow */
if (theModel.get("installProcessed")) {
/* Update the state of the panel */
this.$("#vipInstallStepPanel").show();
this.$("#vipExtensionStepDiv").addClass("complete").removeClass("pending");
this.$("#vipPluginStepDiv").addClass("complete").removeClass("pending");
} else {
this.$("#vipInstallStepPanel").hide();
}
this.$("#vipDialogRememberTitleText").css("padding-top","19px");
return this;
},
confirmName: function() {
var friendlyName = this.$("#friendlyName").val().trim();
var pattern = new RegExp("^([a-zA-Z0-9]|[-_']|\\s)*$");
if(!friendlyName || friendlyName.length === 0 || friendlyName.length > 20 || !pattern.test(friendlyName)){
this.model.set("message", {type:"error", text: translate("message.error.invalid.credentialname")});
return;
} else {
this.model.set("friendlyName", friendlyName);
}
$("#vipMessageBox").removeClass();
$("#vipMessageContent").text("");
$('#vipEnterDeviceName').hide();
$('#vipConfirmFriendlyName').html($('#friendlyName').val());
$('#vipConfirmDeviceName').show();
},
next: function() {
var isIA = this.model.get("credType") === "IA";
if (isIA && !this.model.get("iaExcludeDfp")) { // no tagging at this point
this.model.set("fingerPrint", IaDfp.readFingerprint());
}
var parent = this.options.parentView;
var callback = function(success, ret) {
if (theModel.get("rememberDevice") && !theModel.get("iaExcludeDfp") && success && typeof ret === "object" && isIA && ret.credId && ret.credData) {
// tagging happens on the way back with ticketing response
IaDfp.writeTag(ret.credData, theModel.get("rememberDevice"));
IaDfp.writeTagId(ret.credId);
}
parent.closeAndCallback.apply(parent, arguments);
};
createTicket(this.model, callback);
}
});
/* Counter to make the frames unique within the context of this file */
var sspFrameCounter = 0;
/***************** OOB Helper Functions ********************/
var createSspFrame = function (divId, samlFormUrl, username, password, relayState, userFieldInIdpForm) {
sspFrameCounter = sspFrameCounter + 1;
/* Check if Safari only, embedded Safari on OSX does not report a version */
var useExternalForm = BrowserDetect.browser === "Safari";
var sspFrameName = "sspFrame_provsc_" + sspFrameCounter;
var sspFrameContainer = $("#" + divId + " #vipSspFrameContainer");
// README: This flag is introduced to prevent duplicated firing;
// SAML redirecting process loads a frame of IDP and then SSP;
// if any of them is the same as RP domain, the the populateSspFrame callback will be fired.
var neverFired = true;
var populateSspFrame = function () {
if (neverFired) {
neverFired = false; // stop firing again!!! (almost never happen in real world)
var body;
if (!useExternalForm) {
body = $(this).contents().find("body"); // In IE6 .find() returns an object that .appendTo() is not expecting. javascript error "Invalid Argument". Thus changed it to use .append() for next statement
} else {
/* External forms load contents into the frame container */
body = sspFrameContainer;
}
body.append("");
var form = body.find("form");
if (useExternalForm) {
form.attr('target', sspFrameName);
}
var userField = userFieldInIdpForm||"username";
form.append(""); // Use "username", otherwise custom sspFrameCtor must be used
// use .val() function to handle single/double quote escape so that we don't need to do it by hand.
form.find("input[name='" + userField + "']").val(username);
if (password) {
form.append("");
form.find("input[name='password']").val(password);
}
form.append(""); // Only applies to EG, other IDP may igore this
form.submit();
}
};
if (useExternalForm) {
/* External forms use the name field to identify the target and submits immediately */
$('').appendTo(sspFrameContainer);
populateSspFrame();
} else {
$('').on('load', populateSspFrame).appendTo(sspFrameContainer);
}
};
// Eric wrap with ajax to replace with $.getJSON()
var getOOBJsonpCall = function(divId, oobResult) {
var reqObj = {'_method':'GET'};
reqObj.sspToken = theModel.get('sspToken');
reqObj.requestId = theModel.get('requestId');
var oobGetJSON = function(reqObj, callback) {
$.ajax({
url : sspUrl,
method : "GET",
data : reqObj,
dataType : "jsonp",
success : function(data){
if (isLogEnabled) console.log(data);
if (data !== null && typeof data === 'object'){
if(data.success === true) {
// remove the EG iframe
$("#" + divId + " #vipSspFrameContainer #vipSspFrame").remove();
// remove form that may have been created for the External Forms setup
$("#" + divId + " #vipSspFrameContainer form").remove();
// display ooblink
$("#" + divId + " #vipOoblink").css("display", "block");
$("#" + divId + " #otpInput").focus();
theModel.set('oobList', data.ret);
if (isLogEnabled) console.log(data.ret); // for debug purposes
}
if (isLogEnabled) console.log ("size of OOB is: " + size(data.ret) + " isSMSorEMAILOnly : " + hasSMSOption(data.ret));
//Check if OOB SMS needs to be automatically sent.
if ((size(data.ret) === 1 && hasSMSOption(data.ret)) && data.autoSendSMS.AUTO_SMS_ALLOWED ==='true') {
if (isLogEnabled) console.log('OOB for user Matches Auto SMS or EMAIL Criteria.');
theModel.set('autoSMSOOB', true);
var oobKey = getSMSAndEMAILKeyType(data.ret);
var vOptionSel = theModel.get('oobList')[oobKey];
var decodedOption = $(document.createElement('div')).html(vOptionSel).text();
var reqObj = {
'_method': 'PUT',
'opr': oobKey,
'sspToken': theModel.get('sspToken'),
'requestId': theModel.get('requestId')
};
$.getJSON(sspUrl + "?callback=?", reqObj, function (data) {
if ((data !== null && typeof data === 'object' && data.success === true)) {
if (isLogEnabled) console.log('Auto sending SMS completed successfully.');
theModel.set("message", {
type: "success",
text: translate("auto.message.success.send.securitycode", {decodedOption: decodedOption})
});
} else {
if (isLogEnabled) console.log('Auto sending SMS error.');
var errorMsg = "";
if (data != null)
errorMsg = data.ret.errorMsg;
var allPresent = false;
if (errorMsg != null || errorMsg.trim() != "") {
var keywords = ["sms", "try", "minute"];
allPresent = keywords.every(function(keyword) {
return errorMsg.toLowerCase().indexOf(keyword.toLowerCase()) !== -1;
});
}
if (allPresent) {
theModel.set("message", {
type: "error",
text: errorMsg
});
} else {
theModel.set("message", {
type: "error",
text: translate("message.error.send.securitycode", {decodedOption: decodedOption})
});
}
}
});
} else if((size(data.ret)) >= 1 && data.autoSendSMS.AUTO_SMS_ALLOWED ==='true'){
theModel.set('navigateOOBScreen',true);
}
//regardless of success is true or false, populate the policy to the model
var policy = data.policy;
// Value of RD_ALLOWED from ssp is account level. If user belongs to a group, then replace it with the group policy value.
if (g_isRDAllowed != "unknown") {
data.policy.RD_ALLOWED = g_isRDAllowed;
}
if(policy && typeof policy === "object"){
theModel.set('policy', data.policy);
}
} else {
if (isLogEnabled) console.log('Failed to retrieve security code delivery information.');
}
if (callback !== null) {
callback(true);
}
},
error : function(requestXHR, textStatus) {
if (textStatus === 'timeout') {
if (callback !== null) {
callback(false);
}
}
},
timeout:6000
});
}
oobGetJSON(reqObj, oobResult);
};
var getOOBOptions = function (divId, options, samlFormUrl, sspUrl, callbackResult) {
// add callback;
// IE5 - IE8 use attachEvent("onmessage", handler); Other browsers use addEventListener("message", handler); ????
var eventMethod = window.addEventListener ? "addEventListener" : "attachEvent";
var eventer = window[eventMethod];
var messageEvent = eventMethod == "attachEvent" ? "onmessage" : "message";
var uneventMethod = window.addEventListener ? "removeEventListener" : "detachEvent";
var uneventer = window[uneventMethod];
function OOBResult(result) {
if (isLogEnabled) console.log("getOOBJsonCall result: " + result);
if (callbackResult !== null) {
callbackResult(result);
}
}
var handler = function(event) {
if (entSspUrl === event.origin) { //README: CRITICAL SECURITY CHECK!!!
uneventer(messageEvent, handler, false); // remove callback
var message = JSON.parse(event.data);
if (isLogEnabled) console.log("Message received. Type: [" + message.type + "].");
if (message.type === 'sspSession') {
theModel.set("sspToken", message.sspToken); // this may trigger the display of rememberDevice
getOOBJsonpCall(divId, OOBResult);
}
} else {
if (isLogEnabled) console.log("Message from origin: [" + event.origin + "] ignored.");
}
};
// Listen to message from child iframe
// expect the message of format as stringified {"type":"sspSession", "sspToken":"123456789username"}
eventer(messageEvent, handler,false);
// populate relaystate to the same value the idp is going to be configured to
var relayState = entSspUrl + "/vipssp/ssptoken.v?targetDomain=" + encodeURIComponent(window.location.protocol + "//" + window.location.host);
if(PlatformBrowserSupport.isPostMessageUnsupported()) {
// Using url hash for cross domain call requires exact target url without the hash!
var windowUrl = window.location.href;
if (windowUrl.indexOf("#") >=0) {
windowUrl = windowUrl.substring(0, windowUrl.indexOf('#'));
}
relayState = relayState + "&targetPage=" + encodeURIComponent(windowUrl);
var pollingCnt = 40;
var pollUrlHash = function() {
if (window.location.hash && window.location.hash.length>1 && window.location.hash.indexOf('#sspMessage=')>=0) {
var message = window.location.hash.substring('#sspMessage='.length);
if (isLogEnabled) console.log(message);
window.location.href = window.location.href.substring(0, window.location.href.indexOf('#') + 1);
var event = {'data': message, 'origin': entSspUrl};
handler(event);
} else if (--pollingCnt > 0){
setTimeout(pollUrlHash, 300);
} else {
if (isLogEnabled) console.log("stop polling sspMessage");
}
};
setTimeout(pollUrlHash, 300);
}
// add an iframe, let client override the default logic
var sspFrameCtor = createSspFrame;
if (options.sspFrameCtor && typeof options.sspFrameCtor === 'function') {
sspFrameCtor = options.sspFrameCtor; // Let client override the sspFrameCtor
}
// use options.loginID instead of options.user because user maybe a hash
// loginID is the value entered by the user.
sspFrameCtor(divId, samlFormUrl, options.loginID, options.password, relayState, options.userFieldInIdpForm);
};
/*
* "genTicket()" is executed on the "submission" event, or equivalent,
* with the context of a provsc credential, so a couple of helper methods are accessible; see vip.js for more details
*
* The provisioning credential are decided by policy, "initCredArray" and runtime parameter "provCredType"
*
* Effectively, initCredArray to provisioning credential type mapping is
* (provCredType)
* (Platform does not support RC) => IA
* IA + ProvSC => IA
* RC + ProvSC => RC
* RC + IA + ProvSC => RC
*/
cred.rawGenTicket = function (options, callback) {
var divId = cred.getId();
// rememberDevice display is controlled by the following factors in the order of precedence; see SCView
// 1. forceRememberDeviceDisplay passed in the options
// 2. suggestion from IA response (based on risk rules); if false, then not to display
// 3. valid RC already exist; which indicates this credential is trigger only because IA overrules the RC
// 4. other factors such as sspToken etc
var forceRememberDeviceDisplay = options.forceRememberDeviceDisplay || (options.showRememberDevice === false ? "off" : null) || (options.validRC ? "off" : null);
var rememberDeviceDefault = !!options.rememberDeviceDefault;
var extensionExistValue = options.chromeExtensionExist || options.browserExtensionExist;
var upgradeToTrustedDeviceSkipped = options.upgradeCancelled;
theModel.reset({
user: options.user,
isUserIdHashed : !!options.isUserIdHashed,
password: options.password,
requestId: options.requestId,
forceRememberDeviceDisplay: forceRememberDeviceDisplay,
rememberDeviceDefault: rememberDeviceDefault,
failedRC: options.failedRC,
iaExcludeDfp: !!options.iaExcludeDfp,
hideSubtitle: options.hideSubtitle,
extExist: false,
plgExist: false,
policy: {},
extensionExist: extensionExistValue,
upgradeSkipped: upgradeToTrustedDeviceSkipped,
installProcessed: false,
mobilePushApproved: !!options.mobilePushApproved,
mobilePushTicket: options.mobilePushTicket,
rdBlocked: !!options.rdBlocked
});
new ProvView({id: divId, model: theModel, env: {options:options, callback:callback, context:this}}).render();
};
};
})(vipUnderscore, vipjQuery, vipBackbone);
(function(_, $, Backbone) {
"use strict";
var isLogEnabled = window.isLogEnabled; // actual value is defined in
// vip-js.jsp; this is just to
// remove the dependency.
var catalog = catalogFromUserServices || {};
var translate = underi18n.MessageFactory(catalog);
if (window.rcInitializer) {
if (isLogEnabled)
console.log("WARNING: rcInitializer already exists.");
return;
}
window.rcInitializer = function(spec) {
var cred = this;
/** *************** Models ******************* */
var RCModel = Backbone.Model.extend({
defaults : function() {
return {
username : null,
requestId : null,
ticket : null,
upgradeSkipped : false
};
},
initialize : function() {
this.on("error", function(model, error) {
if (isLogEnabled)
console.log("Error: " + error);
});
},
reset : function(initObj) {
this.set(this.defaults());
if (initObj)
this.set(initObj);
},
progress: function(percent) {
var progressBarWidth = percent * $('.progressBar').width() / 100;
$('.progressBar').find('div').animate({ width: progressBarWidth });
}
});
var theModel = new RCModel();
/** *************** Templates ******************* */
var templateRCOuter = spec.tplRCOuter
|| '
").append(n.parseHTML(a)).find(d):a)}).complete(c&&function(a,b){g.each(c,e||[a.responseText,b,a])}),this},n.expr.filters.animated=function(a){return n.grep(n.timers,function(b){return a===b.elem}).length};var dd=a.document.documentElement;function ed(a){return n.isWindow(a)?a:9===a.nodeType?a.defaultView||a.parentWindow:!1}n.offset={setOffset:function(a,b,c){var d,e,f,g,h,i,j,k=n.css(a,"position"),l=n(a),m={};"static"===k&&(a.style.position="relative"),h=l.offset(),f=n.css(a,"top"),i=n.css(a,"left"),j=("absolute"===k||"fixed"===k)&&n.inArray("auto",[f,i])>-1,j?(d=l.position(),g=d.top,e=d.left):(g=parseFloat(f)||0,e=parseFloat(i)||0),n.isFunction(b)&&(b=b.call(a,c,h)),null!=b.top&&(m.top=b.top-h.top+g),null!=b.left&&(m.left=b.left-h.left+e),"using"in b?b.using.call(a,m):l.css(m)}},n.fn.extend({offset:function(a){if(arguments.length)return void 0===a?this:this.each(function(b){n.offset.setOffset(this,a,b)});var b,c,d={top:0,left:0},e=this[0],f=e&&e.ownerDocument;if(f)return b=f.documentElement,n.contains(b,e)?(typeof e.getBoundingClientRect!==L&&(d=e.getBoundingClientRect()),c=ed(f),{top:d.top+(c.pageYOffset||b.scrollTop)-(b.clientTop||0),left:d.left+(c.pageXOffset||b.scrollLeft)-(b.clientLeft||0)}):d},position:function(){if(this[0]){var a,b,c={top:0,left:0},d=this[0];return"fixed"===n.css(d,"position")?b=d.getBoundingClientRect():(a=this.offsetParent(),b=this.offset(),n.nodeName(a[0],"html")||(c=a.offset()),c.top+=n.css(a[0],"borderTopWidth",!0),c.left+=n.css(a[0],"borderLeftWidth",!0)),{top:b.top-c.top-n.css(d,"marginTop",!0),left:b.left-c.left-n.css(d,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||dd;while(a&&!n.nodeName(a,"html")&&"static"===n.css(a,"position"))a=a.offsetParent;return a||dd})}}),n.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,b){var c=/Y/.test(b);n.fn[a]=function(d){return W(this,function(a,d,e){var f=ed(a);return void 0===e?f?b in f?f[b]:f.document.documentElement[d]:a[d]:void(f?f.scrollTo(c?n(f).scrollLeft():e,c?e:n(f).scrollTop()):a[d]=e)},a,d,arguments.length,null)}}),n.each(["top","left"],function(a,b){n.cssHooks[b]=Mb(l.pixelPosition,function(a,c){return c?(c=Kb(a,b),Ib.test(c)?n(a).position()[b]+"px":c):void 0})}),n.each({Height:"height",Width:"width"},function(a,b){n.each({padding:"inner"+a,content:b,"":"outer"+a},function(c,d){n.fn[d]=function(d,e){var f=arguments.length&&(c||"boolean"!=typeof d),g=c||(d===!0||e===!0?"margin":"border");return W(this,function(b,c,d){var e;return n.isWindow(b)?b.document.documentElement["client"+a]:9===b.nodeType?(e=b.documentElement,Math.max(b.body["scroll"+a],e["scroll"+a],b.body["offset"+a],e["offset"+a],e["client"+a])):void 0===d?n.css(b,c,g):n.style(b,c,d,g)},b,f?d:void 0,f,null)}})}),n.fn.size=function(){return this.length},n.fn.andSelf=n.fn.addBack,"function"==typeof define&&define.amd&&define("jquery",[],function(){return n});var fd=a.jQuery,gd=a.$;return n.noConflict=function(b){return a.$===n&&(a.$=gd),b&&a.jQuery===n&&(a.jQuery=fd),n},typeof b===L&&(a.jQuery=a.$=n),n});
/*! jQuery Migrate v1.2.1 | (c) 2005, 2013 jQuery Foundation, Inc. and other contributors | jquery.org/license */
jQuery.migrateMute===void 0&&(jQuery.migrateMute=!0),function(e,t,n){function r(n){var r=t.console;i[n]||(i[n]=!0,e.migrateWarnings.push(n),r&&r.warn&&!e.migrateMute&&(r.warn("JQMIGRATE: "+n),e.migrateTrace&&r.trace&&r.trace()))}function a(t,a,i,o){if(Object.defineProperty)try{return Object.defineProperty(t,a,{configurable:!0,enumerable:!0,get:function(){return r(o),i},set:function(e){r(o),i=e}}),n}catch(s){}e._definePropertyBroken=!0,t[a]=i}var i={};e.migrateWarnings=[],!e.migrateMute&&t.console&&t.console.log&&t.console.log("JQMIGRATE: Logging is active"),e.migrateTrace===n&&(e.migrateTrace=!0),e.migrateReset=function(){i={},e.migrateWarnings.length=0},"BackCompat"===document.compatMode&&r("jQuery is not compatible with Quirks Mode");var o=e("",{size:1}).attr("size")&&e.attrFn,s=e.attr,u=e.attrHooks.value&&e.attrHooks.value.get||function(){return null},c=e.attrHooks.value&&e.attrHooks.value.set||function(){return n},l=/^(?:input|button)$/i,d=/^[238]$/,p=/^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i,f=/^(?:checked|selected)$/i;a(e,"attrFn",o||{},"jQuery.attrFn is deprecated"),e.attr=function(t,a,i,u){var c=a.toLowerCase(),g=t&&t.nodeType;return u&&(4>s.length&&r("jQuery.fn.attr( props, pass ) is deprecated"),t&&!d.test(g)&&(o?a in o:e.isFunction(e.fn[a])))?e(t)[a](i):("type"===a&&i!==n&&l.test(t.nodeName)&&t.parentNode&&r("Can't change the 'type' of an input or button in IE 6/7/8"),!e.attrHooks[c]&&p.test(c)&&(e.attrHooks[c]={get:function(t,r){var a,i=e.prop(t,r);return i===!0||"boolean"!=typeof i&&(a=t.getAttributeNode(r))&&a.nodeValue!==!1?r.toLowerCase():n},set:function(t,n,r){var a;return n===!1?e.removeAttr(t,r):(a=e.propFix[r]||r,a in t&&(t[a]=!0),t.setAttribute(r,r.toLowerCase())),r}},f.test(c)&&r("jQuery.fn.attr('"+c+"') may use property instead of attribute")),s.call(e,t,a,i))},e.attrHooks.value={get:function(e,t){var n=(e.nodeName||"").toLowerCase();return"button"===n?u.apply(this,arguments):("input"!==n&&"option"!==n&&r("jQuery.fn.attr('value') no longer gets properties"),t in e?e.value:null)},set:function(e,t){var a=(e.nodeName||"").toLowerCase();return"button"===a?c.apply(this,arguments):("input"!==a&&"option"!==a&&r("jQuery.fn.attr('value', val) no longer sets properties"),e.value=t,n)}};var g,h,v=e.fn.init,m=e.parseJSON,y=/^([^<]*)(<[\w\W]+>)([^>]*)$/;e.fn.init=function(t,n,a){var i;return t&&"string"==typeof t&&!e.isPlainObject(n)&&(i=y.exec(e.trim(t)))&&i[0]&&("<"!==t.charAt(0)&&r("$(html) HTML strings must start with '<' character"),i[3]&&r("$(html) HTML text after last tag is ignored"),"#"===i[0].charAt(0)&&(r("HTML string cannot start with a '#' character"),e.error("JQMIGRATE: Invalid selector string (XSS)")),n&&n.context&&(n=n.context),e.parseHTML)?v.call(this,e.parseHTML(i[2],n,!0),n,a):v.apply(this,arguments)},e.fn.init.prototype=e.fn,e.parseJSON=function(e){return e||null===e?m.apply(this,arguments):(r("jQuery.parseJSON requires a valid JSON string"),null)},e.uaMatch=function(e){e=e.toLowerCase();var t=/(chrome)[ \/]([\w.]+)/.exec(e)||/(webkit)[ \/]([\w.]+)/.exec(e)||/(opera)(?:.*version|)[ \/]([\w.]+)/.exec(e)||/(msie) ([\w.]+)/.exec(e)||0>e.indexOf("compatible")&&/(mozilla)(?:.*? rv:([\w.]+)|)/.exec(e)||[];return{browser:t[1]||"",version:t[2]||"0"}},e.browser||(g=e.uaMatch(navigator.userAgent),h={},g.browser&&(h[g.browser]=!0,h.version=g.version),h.chrome?h.webkit=!0:h.webkit&&(h.safari=!0),e.browser=h),a(e,"browser",e.browser,"jQuery.browser is deprecated"),e.sub=function(){function t(e,n){return new t.fn.init(e,n)}e.extend(!0,t,this),t.superclass=this,t.fn=t.prototype=this(),t.fn.constructor=t,t.sub=this.sub,t.fn.init=function(r,a){return a&&a instanceof e&&!(a instanceof t)&&(a=t(a)),e.fn.init.call(this,r,a,n)},t.fn.init.prototype=t.fn;var n=t(document);return r("jQuery.sub() is deprecated"),t},e.ajaxSetup({converters:{"text json":e.parseJSON}});var b=e.fn.data;e.fn.data=function(t){var a,i,o=this[0];return!o||"events"!==t||1!==arguments.length||(a=e.data(o,t),i=e._data(o,t),a!==n&&a!==i||i===n)?b.apply(this,arguments):(r("Use of jQuery.fn.data('events') is deprecated"),i)};var j=/\/(java|ecma)script/i,w=e.fn.andSelf||e.fn.addBack;e.fn.andSelf=function(){return r("jQuery.fn.andSelf() replaced by jQuery.fn.addBack()"),w.apply(this,arguments)},e.clean||(e.clean=function(t,a,i,o){a=a||document,a=!a.nodeType&&a[0]||a,a=a.ownerDocument||a,r("jQuery.clean() is deprecated");var s,u,c,l,d=[];if(e.merge(d,e.buildFragment(t,a).childNodes),i)for(c=function(e){return!e.type||j.test(e.type)?o?o.push(e.parentNode?e.parentNode.removeChild(e):e):i.appendChild(e):n},s=0;null!=(u=d[s]);s++)e.nodeName(u,"script")&&c(u)||(i.appendChild(u),u.getElementsByTagName!==n&&(l=e.grep(e.merge([],u.getElementsByTagName("script")),c),d.splice.apply(d,[s+1,0].concat(l)),s+=l.length));return d});var Q=e.event.add,x=e.event.remove,k=e.event.trigger,N=e.fn.toggle,T=e.fn.live,M=e.fn.die,S="ajaxStart|ajaxStop|ajaxSend|ajaxComplete|ajaxError|ajaxSuccess",C=RegExp("\\b(?:"+S+")\\b"),H=/(?:^|\s)hover(\.\S+|)\b/,A=function(t){return"string"!=typeof t||e.event.special.hover?t:(H.test(t)&&r("'hover' pseudo-event is deprecated, use 'mouseenter mouseleave'"),t&&t.replace(H,"mouseenter$1 mouseleave$1"))};e.event.props&&"attrChange"!==e.event.props[0]&&e.event.props.unshift("attrChange","attrName","relatedNode","srcElement"),e.event.dispatch&&a(e.event,"handle",e.event.dispatch,"jQuery.event.handle is undocumented and deprecated"),e.event.add=function(e,t,n,a,i){e!==document&&C.test(t)&&r("AJAX events should be attached to document: "+t),Q.call(this,e,A(t||""),n,a,i)},e.event.remove=function(e,t,n,r,a){x.call(this,e,A(t)||"",n,r,a)},e.fn.error=function(){var e=Array.prototype.slice.call(arguments,0);return r("jQuery.fn.error() is deprecated"),e.splice(0,0,"error"),arguments.length?this.bind.apply(this,e):(this.triggerHandler.apply(this,e),this)},e.fn.toggle=function(t,n){if(!e.isFunction(t)||!e.isFunction(n))return N.apply(this,arguments);r("jQuery.fn.toggle(handler, handler...) is deprecated");var a=arguments,i=t.guid||e.guid++,o=0,s=function(n){var r=(e._data(this,"lastToggle"+t.guid)||0)%o;return e._data(this,"lastToggle"+t.guid,r+1),n.preventDefault(),a[r].apply(this,arguments)||!1};for(s.guid=i;a.length>o;)a[o++].guid=i;return this.click(s)},e.fn.live=function(t,n,a){return r("jQuery.fn.live() is deprecated"),T?T.apply(this,arguments):(e(this.context).on(t,this.selector,n,a),this)},e.fn.die=function(t,n){return r("jQuery.fn.die() is deprecated"),M?M.apply(this,arguments):(e(this.context).off(t,this.selector||"**",n),this)},e.event.trigger=function(e,t,n,a){return n||C.test(e)||r("Global events are undocumented and deprecated"),k.call(this,e,t,n||document,a)},e.each(S.split("|"),function(t,n){e.event.special[n]={setup:function(){var t=this;return t!==document&&(e.event.add(document,n+"."+e.guid,function(){e.event.trigger(n,null,t,!0)}),e._data(this,n,e.guid++)),!1},teardown:function(){return this!==document&&e.event.remove(document,n+"."+e._data(this,n)),!1}}})}(jQuery,window);
underscore = vipUnderscore;
Backbone = vipBackbone;